File Coverage

blib/lib/Nagios/Plugin/SNMP.pm
Criterion Covered Total %
statement 27 344 7.8
branch 0 134 0.0
condition 0 21 0.0
subroutine 9 32 28.1
pod 9 13 69.2
total 45 544 8.2


line stmt bran cond sub pod time code
1             package Nagios::Plugin::SNMP;
2              
3             =pod
4              
5             =head1 NAME
6              
7             Nagios::Plugin::SNMP - Helper module to make writing SNMP-based plugins for Nagios easier.
8              
9             =head1 SYNOPSIS
10              
11             This module requires Net::SNMP for its' SNMP functionality; it
12             subclasses Nagios::Plugin.
13              
14             It includes routines to do the following:
15              
16             =head2 Parse and process common SNMP arguments:
17              
18             * --warning|-w: Warning threshold [optional]
19             * --critical|-c: Warning threshold [optional]
20             * --hostname|-H: SNMP device to query
21             * --alt-host|--ah: Additional SNMP devices to query
22             * --port|-p: Port on remote device to connect to [default 161]
23             * --snmp-local-ip: Local IP to bind to for outgoing requests
24             * --snmp-version: SNMP version (1, 2c, 3)
25             * --snmp-timeout: Connect timeout in seconds [default 0]
26             * --snmp-debug: Turn on Net::SNMP debugging
27             * --snmp-max-msg-size N: Set maximum SNMP message size in bytes
28             * --rocommunity: Read-only community string for SNMP 1/v2c
29             * --auth-username: Auth username for SNMP v3
30             * --auth-password: Auth password for SNMP v3
31             * --auth-protocol: Auth protocol for SNMP v3 (defaults to md5)
32             * Connect to an SNMP device
33             * Perform a get() or walk() request, each method does 'the right
34             thing' based on the version of SNMP selected by the user.
35              
36             =head2 noSuchObject and noSuchInstance behavior
37              
38             You can configure your plugin to automatically exit with UNKNOWN if
39             OIDs you query for on the remote agent do not exist on the agent by
40             setting 'error_on_no_such' to 1 in new(). If you set it to 0 or
41             do not specify the option at all, then results from get() and walk()
42             will have the string tokens noSuchObject or noSuchInstance as values
43             for OIDs that the agent does not support.
44              
45             This option exists as some scripts will query agents for MIBs that have
46             *some* tables or scalars that may or may not be supported depending on the
47             remote agent version, operating system, etc. In this case
48             it is nice to just parse the returns from get() and walk() to programatically
49             determine if support exists.
50              
51             In other cases the script may be querying a
52             clustered application with multiple agents where only one agent will respond
53             with the OIDs queried; the others will return noSuch* values .. for this case
54             having the script exit with error if *none* of the agents return valid values
55             makes sense at least one agent MUST have the OIDs for the check to
56             succeed and all agents are known to respond to the OIDs in question
57             (set error_on_no_such to 1).
58              
59             my $plugin = Nagios::Plugin::SNMP->new(
60             'error_on_no_such' => 1
61             );
62              
63             =head2 Handle deltas for counters
64              
65             my $plugin = Nagios::Plugin::SNMP->new(
66             'process_deltas' => {
67             'cache' => { 'type' => 'memcache' },
68             'default_interval' => 300, # seconds
69             'delta_compute_function' => \&my_delta_function
70             }
71             );
72              
73             Will use any Cache::Cache compliant data cache to store counter
74             values and return deltas between counters to the end user. In order
75             to do this, Nagios::Plugin::SNMP must be passed in a cache class name.
76             Each cache type will cause Nagios::Plugin::SNMP to add required
77             arguments for the cache type in question. Additionally, enabling
78             counter delta code causes the script to require an interval for
79             time between checks. From the command line this can be specified
80             with --check-interval. The developer can pass in a default using
81             the 'default_interval' parameter.
82              
83             * cache => { 'type' => 'memcache' }
84              
85             Causes Nagios::Plugin::SNMP to use Cache::Memcached for
86             data persistence; also makes the following arguments to the plugin
87             available to the user. Defaults for both can be provided in the
88             memcache option hash as memcache_addr and memcache_port.
89             * --memcache-addr - IP address of Memcache instance
90             * --memcache-port - TCP port number memcache is listening on
91              
92             my $plugin = Nagios::Plugin::SNMP->new(
93             'process_deltas' => {
94             'cache' => {
95             'type' => 'memcache',
96             'options => { 'memcache_port' => 11211,
97             'memcache_addr' => 127.0.0.1
98             },
99             'default_interval' => 300, # seconds
100             'delta_compute_function' => \&my_delta_function
101             }
102             );
103              
104             Currently ONLY Cache::Memcached is supported.
105              
106             * 'process_deltas' => { 'delta_compute_function' => \&my_delta_function }
107              
108             Callback to allow user to pass in a function that will compute a
109             smarter delta :). The default function purely does the following:
110             * If previous value does not exist, it stores the current value
111             and returns -0
112             * If previous value exists and is > 0, stores the current value and
113             returns the delta between the current value and previous value
114             * If delta between the two values is < 0, returns -0 as counter
115             has wrapped.
116              
117             sub my_delta_function {
118             my ($self, $args) = @_;
119            
120             my $previous_value = $args->{'previous_value'};
121             my $current_value = $args->{'current_value'};
122             my $interval = $args->{'interval'};
123             my $previous_run_at = $args->{'previous_run_at'};
124              
125             my ($value_to_store, $delta) = ();
126            
127             # Processing code
128              
129             return ($value_to_store, $delta);
130             }
131            
132             =head3 get_deltas(@oids)
133              
134             Will query the agent for values associated with the SCALAR OIDs passed
135             in and return a hash with the results of the query; the value for each
136             oid will be the delta, massaged as needed by the built in delta-computation
137             function or your own function.
138              
139             =head3 get_delta_for_value($key, $value);
140              
141             Will perform delta computation on the passed in value; using
142             the key passed in in place of the OID that would be used in
143             get_deltas as the hash key. This routine can be used by the end
144             user for metrics that are created by the plugin developer as opposed to
145             counters retrieved by the SNMP agent.
146              
147             =head2 Other methods
148              
149             =cut
150              
151 1     1   23546 use strict;
  1         2  
  1         58  
152              
153             require Exporter;
154 1     1   5 use base qw(Exporter Nagios::Plugin);
  1         2  
  1         1253  
155              
156 1     1   60901 use Net::SNMP;
  1         100126  
  1         150  
157              
158             # Have to copy, inheritence doesn't work for these
159 1     1   8 use constant OK => 0;
  1         1  
  1         53  
160 1     1   5 use constant WARNING => 1;
  1         1  
  1         32  
161 1     1   4 use constant CRITICAL => 2;
  1         2  
  1         29  
162 1     1   3 use constant UNKNOWN => 3;
  1         1  
  1         30  
163 1     1   4 use constant DEPENDENT => 4;
  1         1  
  1         3202  
164              
165             our @EXPORT = qw(OK WARNING CRITICAL UNKNOWN DEPENDENT);
166              
167             our $VERSION = '1.2';
168              
169             our $SNMP_USAGE = <
170             --hostname|-H HOST --port|-p INT --snmp-version 1|2c|3 \\
171             [--snmp-timeout INT] \\
172             [--snmp-local-ip IP] \\
173             [--warning|-w STRING] [--critical|-c STRING] \\
174             [--snmp-debug] \\
175             [--snmp-max-msg-size N] \\
176             [--alt-host HOST_1 ... --alt-host HOST_N] \\
177             {
178             [--rocommunity S] | \\
179             [--auth-username S --auth-password S [--auth-protocol S]]
180             }
181             EOF
182              
183             our %OS_TYPES = qw(
184             .1.3.6.1.4.1.8072.3.2.1 hpux
185             .1.3.6.1.4.1.8072.3.2.2 sunos4
186             .1.3.6.1.4.1.8072.3.2.3 solaris
187             .1.3.6.1.4.1.8072.3.2.4 osf
188             .1.3.6.1.4.1.8072.3.2.5 ultrix
189             .1.3.6.1.4.1.8072.3.2.6 hpux10
190             .1.3.6.1.4.1.8072.3.2.7 netbsd1
191             .1.3.6.1.4.1.8072.3.2.8 freebsd
192             .1.3.6.1.4.1.8072.3.2.9 irix
193             .1.3.6.1.4.1.8072.3.2.10 linux
194             .1.3.6.1.4.1.8072.3.2.11 bsdi
195             .1.3.6.1.4.1.8072.3.2.12 openbsd
196             .1.3.6.1.4.1.8072.3.2.13 win32
197             .1.3.6.1.4.1.8072.3.2.14 hpux11
198             .1.3.6.1.4.1.8072.3.2.255 unknown
199             );
200              
201             sub new {
202              
203 0     0 0   my $class = shift;
204 0           my %args = (@_);
205              
206 0           $args{'usage'} .= $SNMP_USAGE;
207              
208 0           my $process_delta_opts = undef;
209              
210             # have to do this before SUPER call
211 0 0         if (exists $args{'process_deltas'}) {
212 0           $process_delta_opts = $args{'process_deltas'};
213 0           $args{'usage'} .= " --check-interval seconds\n";
214              
215 0 0 0       if ((exists $process_delta_opts->{'cache'}) &&
216             (exists $process_delta_opts->{'cache'}->{'type'})) {
217              
218 0           my $ct = lc($process_delta_opts->{'cache'}->{'type'});
219              
220 0 0         if ($ct eq 'memcache') {
221 0           $args{'usage'} .= <
222             --memcache-addr listening hostname or IP
223             --memcache-port listening port
224             EOF
225             }
226              
227             }
228 0           delete $args{'process_deltas'};
229             }
230              
231             # For multiple host checks, developer might want script to
232             # exit with error if OID does not exist on any host checked;
233             # this will cause a check for noSuchObject and noSuchInstance
234             # to happen for every result returned by the remote agent ..
235             # with this set to 1 the agent will exit with error if noSuch*
236             # errors are found in result sets for all hosts.
237              
238 0           my $die_on_no_such = 0;
239              
240 0 0         if (exists $args{'error_on_no_such'}) {
241 0           $die_on_no_such = $args{'error_on_no_such'};
242             }
243 0           delete $args{'error_on_no_such'};
244              
245 0           my $self = $class->SUPER::new(%args);
246              
247 0 0         if (defined $process_delta_opts) {
248 0           $self->_setup_delta_cache_options($process_delta_opts);
249             }
250              
251             # Add standard SNMP options to the plugin
252 0           $self->_snmp_add_options();
253              
254             # Hold the SNMP sessions we are using. Multiple
255             # potential hosts can be specified using one or more
256             # --alt-host arguments in addition to the single
257             # --hostname argument given on the command line.
258 0           $self->{'_SNMP_SESSIONS'} = [];
259              
260 0           $self->{'_SNMP_DIE_ON_NO_SUCH'} = $die_on_no_such;
261              
262             # Hold the name of the host used for polling when multiple
263             # potential hosts are specified using --hostname and
264             # one or more --alt-host options on the command line
265 0           $self->{'_SNMP_POLLED_HOST'} = '';
266              
267 0           return $self;
268             }
269              
270             sub start_timer {
271 0     0 0   my $self = shift;
272              
273 0 0 0       if ( (defined $self->opts->get('timeout')) &&
274             ($self->opts->get('timeout') > 0) ) {
275 0           alarm($self->opts->get('timeout'));
276             }
277             }
278              
279             # Add Nagios::Plugin options related to SNMP to the plugin
280              
281             sub _snmp_add_options {
282              
283 0     0     my $self = shift;
284              
285 0           $self->add_arg(
286             'spec' => 'snmp-version=s',
287             'help' => '--snmp-version 1|2c|3 [default 3]',
288             'required' => 1,
289             'default' => '3'
290             );
291              
292 0           $self->add_arg(
293             'spec' => 'rocommunity=s',
294             'help' => "--rocommunity NAME\n Community name: SNMP 1|2c ONLY",
295             'required' => 0,
296             'default' => ''
297             );
298              
299 0           $self->add_arg(
300             'spec' => 'auth-username=s',
301             'help' => "--auth-username USER\n Auth username: SNMP 3 only",
302             'required' => 0,
303             'default' => ''
304             );
305              
306 0           $self->add_arg(
307             'spec' => 'auth-password=s',
308             'help' => "--auth-password PASS\n Auth password: SNMP 3 only",
309             'required' => 0,
310             'default' => ''
311             );
312              
313 0           $self->add_arg(
314             'spec' => 'auth-protocol=s',
315             'help' => "--auth-protocol PROTO\n" .
316             " Auth protocol: SNMP 3 only [default md5]",
317             'required' => 0,
318             'default' => 'md5'
319             );
320              
321 0           $self->add_arg(
322             'spec' => 'port|p=s',
323             'help' => "--port INT\n SNMP agent port [default 161]",
324             'required' => 0,
325             'default' => '161'
326             );
327              
328 0           $self->add_arg(
329             'spec' => 'hostname|H=s',
330             'help' => "-H, --hostname\n Host to check NAME|IP",
331             'required' => 1
332             );
333              
334 0           $self->add_arg(
335             'spec' => 'alt-host|ah|A=s@',
336             'help' => <
337             -A, --ah, --alt-host NAME|IP
338             Additional hosts to attempt to connect to in addition to the host given
339             with the --hostname option, pass in one host per --alt-host option .
340             Use if you want to check a cluster of hosts in which only *ONE* has an
341             active agent that will respond to a query properly. Each host will be
342             tried in turn for each get() or walk() request until either a valid
343             response is received or all hosts fail to respond or return noSuchObject
344             or noSuchInstance. An error will only be thrown by the plugin if none
345             of the hosts passed in with --hostname and --alt-host return a valid
346             response.
347             EOF
348             'required' => 0,
349             'default' => []
350             );
351              
352 0           $self->add_arg(
353             'spec' => 'snmp-timeout=i',
354             'help' => "--snmp-timeout INT\n" .
355             " Timeout for SNMP queries [default - none]",
356             'default' => 0
357             );
358              
359 0           $self->add_arg(
360             'spec' => 'snmp-debug',
361             'help' => "--snmp-debug [default off]",
362             'default' => 0
363             );
364              
365 0           $self->add_arg(
366             'spec' => 'warning|w=s',
367             'help' => "-w, --warning STRING [optional]",
368             'required' => 0
369             );
370              
371 0           $self->add_arg(
372             'spec' => 'critical|c=s',
373             'help' => "-c, --critical STRING",
374             'required' => 0
375             );
376              
377 0           $self->add_arg(
378             'spec' => 'snmp-local-ip=s',
379             'help' => "--snmp-local-ip IP-ADDRESS\n" .
380             " Local IP address to send traffic on [optional]",
381             'default' => ''
382             );
383              
384 0           $self->add_arg(
385             'spec' => 'snmp-max-msg-size=i',
386             'help' => "--snmp-max-msg-size BYTES\n" .
387             " Specify SNMP maximum messages size [default 1470]",
388             'default' => '1470'
389             );
390              
391             }
392              
393             =pod
394              
395             =head3 _setup_delta_cache_options
396              
397             This method adds arguments to the Nagios::Plugin instance based on
398             the type of cache along with making --check-interval required
399             as a plugin needs to know the interval between plugin calls in
400             order to calculate deltas and delta variance properly.
401              
402             'process_deltas' => {
403             'cache' => {
404             'type' => 'memcache',
405             'options => { 'memcache_port' => 11211,
406             'memcache_addr' => 127.0.0.1
407             },
408             'default_interval' => 300, # seconds
409             'delta_compute_function' => \&my_delta_function
410             }
411             }
412              
413             =cut
414              
415             sub _setup_delta_cache_options {
416              
417 0     0     my ($self, $opts) = @_;
418              
419 0 0         if (! exists $opts->{'cache'}) {
420 0           $self->die(q{'process_deltas' specified but required hash key}
421             . q{'cache' does not exist!});
422             }
423              
424 0 0         if (! ref($opts->{'cache'}) eq 'HASH') {
425 0           $self->die(q{'process_deltas' specified but key 'cache' does not}
426             . q{contain a hash reference of options for cache type!});
427             }
428              
429 0 0         if (! exists $opts->{'cache'}->{'type'}) {
430 0           $self->die(q{'process_deltas' specified but hash ref 'cache' does }
431             . q{not specify a cache type with key 'type'!});
432             }
433              
434             # Add the --check-interval option - required for any delta checks
435              
436 0           my $default_check_interval = 300;
437              
438 0 0         if (exists $opts->{'default_interval'}) {
439 0           $default_check_interval = $opts->{'default_interval'};
440             }
441              
442             $self->add_arg(
443 0           'spec' => 'check-interval=i',
444             'help' => q{--check-interval interval between checks in seconds}
445             . qq{ [default interval $default_check_interval seconds]},
446             'required' => 0,
447             'default' => $default_check_interval,
448             );
449              
450 0           $self->{'_SNMP_PROCESS_DELTAS'} = {};
451              
452             # Cache specific options
453              
454 0           my $cache_type = lc($opts->{'cache'}->{'type'});
455              
456 0 0         if ($cache_type eq 'memcache') {
457              
458 0           eval "use Cache::Memcached";
459              
460 0 0         if ($@) {
461 0           $self->die(q{Delta caching with Memcache requested }
462             . qq{but can't use Cache::Memcached: $@});
463             }
464              
465 0           my $default_memcache_addr = undef;
466 0           my $addr_required = 1;
467 0           my $addr_default = "";
468              
469 0 0         if (exists $opts->{'cache'}->{'options'}->{'memcache_addr'}) {
470 0           $addr_required = 0;
471 0           $default_memcache_addr =
472             $opts->{'cache'}->{'options'}->{'memcache_addr'};
473 0           $addr_default = qq{ (default $default_memcache_addr)};
474             }
475              
476 0           my $default_memcache_port = undef;
477 0           my $port_required = 1;
478 0           my $port_default = "";
479              
480 0 0         if (exists $opts->{'cache'}->{'options'}->{'memcache_port'}) {
481 0           $port_required = 0;
482 0           $default_memcache_port =
483             $opts->{'cache'}->{'options'}->{'memcache_port'};
484 0           $port_default = qq{ (default $default_memcache_port)};
485             }
486              
487             $self->add_arg(
488 0           'spec' => 'memcache-addr|ma=s',
489             'required' => $addr_required,
490             'help' => "--ma, --memcache-addr\n"
491             . qq( Host memcache runs on${addr_default}. Cache is)
492             . q{ required to store deltas for counters for this script. },
493             'default' => $default_memcache_addr
494             );
495              
496 0 0         if ($addr_required == 1) {
497 0           $self->{'usage'} .= q{ --memcache-addr memcache IP or hostname};
498             }
499              
500 0 0         if ($port_required == 1) {
501 0           $self->{'usage'} .= q{ --memcache-port memcache port number}
502             }
503              
504             $self->add_arg(
505 0           'spec' => 'memcache-port|mp=i',
506             'required' => $port_required,
507             'help' => "--mp, --memcache-port\n"
508             . qq( Port number memcache runs on${port_default}. Cache is)
509             . q{ required to store deltas for counters for this script. },
510             'default' => $default_memcache_port
511             );
512              
513             }
514             else {
515 0           $self->die(qq{Unsupported cache type '$cache_type'});
516             }
517              
518 0           $self->{'_SNMP_PROCESS_DELTAS'}->{'cache_type'} = $cache_type;
519              
520 0 0         if (exists $opts->{'delta_compute_function'}) {
521 0           my $callback = $opts->{'delta_compute_function'};
522            
523 0 0         if (! ref($callback) eq 'CODE') {
524 0           $self->die(q{'process_deltas' option 'delta_compute_function'}
525             . q{ is not a reference to a function!});
526             }
527              
528             # Create the callback key here, if the key does not exist
529             # we are using the built-in compute delta function.
530 0           $self->{'_SNMP_PROCESS_DELTAS'}->{'callback'} =
531             $opts->{'delta_compute_function'};
532             }
533              
534             }
535              
536             =pod
537              
538             =head3 _initialize_delta_cache()
539            
540             Initialize cache for processing deltas. All validation of cache
541             options is done in _setup_delta_cache_options.
542              
543             =cut
544              
545             sub _initialize_delta_cache {
546              
547 0     0     my $self = shift;
548            
549 0           my $opts = $self->{'_SNMP_PROCESS_DELTAS'};
550              
551 0           my $cache_type = $opts->{'cache_type'};
552              
553 0 0         if ($cache_type eq 'memcache') {
554              
555 0           my $addr = $self->opts->get('memcache-addr');
556 0           my $port = $self->opts->get('memcache-port');
557              
558 0           eval <
559             use Cache::Memcached;
560              
561             \$self->{'_SNMP_PROCESS_DELTAS'}->{'cache'} =
562             Cache::Memcached->new({'servers' => [ "${addr}:${port}" ] });
563             EOF
564              
565 0           my $error = $@;
566              
567 0 0         $self->die("Cannot instantiate memcached instance: $error")
568             if $error;
569             }
570              
571             }
572              
573             =pod
574              
575             =head2 _get_cache()
576              
577             Return cache instance after ensuring it exists and is valid. Exit with
578             error if it is not.
579              
580             =cut
581              
582             sub _get_cache {
583              
584 0     0     my $self = shift;
585              
586 0 0         if (! exists $self->{'_SNMP_PROCESS_DELTAS'}) {
587 0           $self->die("Cache requested but delta processing not requested!");
588             }
589              
590 0           my $spd = $self->{'_SNMP_PROCESS_DELTAS'};
591              
592 0 0 0       if ((! exists $spd->{'cache'}) ||
593             (! ref($self->{'cache'}) =~ m/^Cache::/)) {
594 0           $self->die("Cache requested but never initialized!");
595             }
596              
597 0           return $spd->{'cache'};
598             }
599              
600             =pod
601              
602             =head2 _get_from_cache($key)
603              
604             Return the value associated with $key from the cache; if value does
605             not exist, returns undef. If cache is invalid, will exit with
606             error.
607              
608             =cut
609              
610             sub _get_from_cache {
611 0     0     my ($self, $key) = @_;
612 0           my $cache = $self->_get_cache();
613 0           my $tv_ref = $cache->get($key);
614 0 0         $tv_ref = { 'timestamp' => 0, 'value' => undef } if ! defined $tv_ref;
615              
616 0 0         if ($self->opts->get('snmp-debug') == 1) {
617 0           my $ts = 'NOT DEFINED';
618 0 0         $ts = $tv_ref->{'timestamp'} if defined $tv_ref->{'timestamp'};
619 0           my $v = 'NOT DEFINED';
620 0 0         $v = $tv_ref->{'value'} if defined $tv_ref->{'value'};
621 0           $self->debug(qq{_get_from_cache: $cache->get($key) returns }
622             . qq{timestamp:$ts value:$v});
623             }
624 0           return ( $tv_ref->{'timestamp'}, $tv_ref->{'value'} );
625             }
626              
627             =pod
628              
629             =head2 _store_in_cache($key, $value)
630              
631             Store a value in the cache using the passed in key.
632              
633             =cut
634              
635             sub _store_in_cache {
636              
637 0     0     my ($self, $key, $value) = @_;
638              
639              
640 0           my $cache = $self->_get_cache();
641 0           my $now = time();
642 0           my $complex_value = { 'value' => $value, 'timestamp' => $now };
643 0           my $result = $cache->set($key, $complex_value);
644              
645 0           $self->debug("_store_in_cache: set($key, $value / $now) returns $result");
646              
647 0 0         $self->die(qq{Unable to store ($key -> $value / $now) in cache, please}
648             . qq{ check cache configuration parameters!}) if $result == 0;
649              
650 0           return $value;
651             }
652              
653             =pod
654              
655             =head2 get_cache_key_for($key)
656              
657             Returns a unique key that can be used to retrieve a value from the cache;
658             feel free to override this with a different unique key algorithm if the
659             default does not suit you (by subclassing Nagios::Plugin::SNMP). By
660             default, the unique key will be made by concatenating the following
661             values, separated by colons:
662             * hostname of the host being checked
663             * port of the host being checked
664             * user provided key (in the case of get_delta_for_value) or OID of
665             the scalar requested (in the case of get_deltas().
666              
667             =cut
668              
669             sub get_cache_key_for {
670              
671 0     0 1   my ($self, $key) = @_;
672              
673 0           my $hostname = $self->opts->get('hostname');
674 0 0         my $port = (defined $self->opts->get('port'))
675             ? $self->opts->get('port') : 0;
676              
677 0           my $cache_key = join(q{:}, $hostname, $port, $key);
678              
679 0           $self->debug("get_cache_key_for: $key -> $cache_key");
680              
681 0           return $cache_key;
682             }
683              
684             =pod
685              
686             =head3 _snmp_validate_opts() - Validate passed in SNMP options
687              
688             This method validates that any options passed to the plugin using
689             this library make sense. Rules:
690              
691             =over 4
692              
693             * If SNMP is version 1 or 2c, rocommunity must be set
694             * If SNMP is version 3, auth-username and auth-password must be set
695              
696             =back
697              
698             =cut
699              
700             sub _snmp_validate_opts {
701              
702 0     0     my $self = shift;
703              
704 0           my $opts = $self->opts;
705              
706 0 0         if ($opts->get('snmp-version') eq '3') {
707              
708 0           my @errors;
709              
710 0           for my $p (qw(auth-username auth-password auth-protocol)) {
711 0 0         push(@errors, $p) if $opts->get($p) eq '';
712             }
713              
714 0 0         die "SNMP parameter validation failed. Missing: " .
715             join(', ', @errors) if scalar(@errors) > 0;
716              
717             } else {
718              
719 0 0         die "SNMP parameter validation failed. Missing rocommunity!"
720             if $opts->get('rocommunity') eq '';
721              
722             }
723              
724 0 0         if ($opts->get('snmp-local-ip') ne '') {
725 0           my $ip = $opts->get('snmp-local-ip');
726 0 0         die "SNMP local bind IP address is invalid!"
727             unless $ip =~ m/^(?:[0-9]{1,3}){4}$/;
728             }
729              
730 0           return 1;
731              
732             }
733              
734             =pod
735              
736             =head3 connect() - Establish SNMP session
737              
738             Attempts to connect to the remote system specified in the
739             command-line arguments; will die() with an error message if the
740             session creation fails.
741              
742             =cut
743              
744             sub connect {
745            
746 0     0 1   my $self = shift;
747              
748 0           $self->_snmp_validate_opts();
749              
750 0           my $opts = $self->opts;
751              
752 0           my @args;
753              
754 0           my $version = $opts->get('snmp-version');
755              
756 0           my @hosts = ($opts->get('hostname'));
757              
758 0           push(@hosts, @{$opts->get('alt-host')})
  0            
759 0 0         if (scalar(@{$opts->get('alt-host')}) > 0);
760            
761 0           my @sessions = ();
762 0           my @errors = ();
763              
764 0           for my $host (@hosts) {
765 0           push(@args, '-version' => $opts->get('snmp-version'));
766 0           push(@args, '-hostname' => $host);
767 0           push(@args, '-port' => $opts->get('port'));
768 0 0         push(@args, '-timeout' => $opts->get('snmp-timeout'))
769             if ($opts->get('snmp-timeout') > 0);
770 0           push(@args, '-debug' => $opts->get('snmp-debug'));
771              
772 0 0         if ($version eq '3') {
773 0           push(@args, '-username' => $opts->get('auth-username'));
774 0           push(@args, '-authpassword' => $opts->get('auth-password'));
775 0           push(@args, '-authprotocol' => $opts->get('auth-protocol'));
776             } else {
777 0           push(@args, '-community' => $opts->get('rocommunity'));
778             }
779              
780 0 0         push(@args, '-localaddr' => $opts->get('snmp-local-ip'))
781             if $opts->get('snmp-local-ip') ne '';
782              
783 0 0         push(@args, '-maxMsgSize' => $opts->get('snmp-max-msg-size'))
784             if $opts->get('snmp-max-msg-size') ne '';
785              
786 0           my ($session, $error) = Net::SNMP->session(@args);
787              
788 0 0         if ($error ne '') {
789 0           push(@errors, "$host - $error");
790             }
791             else {
792 0           push(@sessions, $session);
793             }
794              
795             }
796              
797 0 0 0       if ( (scalar(@errors) > 0) && (scalar(@errors) == scalar(@sessions)) ) {
798 0           $self->die(qq{Net-SNMP session creation failed for all hosts: }
799             . join(', ', @errors));
800             }
801              
802 0           $self->{'_SNMP_SESSIONS'} = \@sessions;
803              
804 0           return $self;
805              
806             }
807              
808             =pod
809              
810             =head3 get(@oids) - Perform an SNMP get request
811              
812             Performs an SNMP get request on each passed in OID; returns results
813             as a hash reference where keys are the passed in OIDs and the values are
814             the values returned from the Net::SNMP get() calls.
815              
816             =cut
817              
818             sub get {
819              
820 0     0 1   my $self = shift;
821 0           my @oids = @_;
822              
823 0 0         die "Missing OIDs to get!" unless scalar(@oids) > 0;
824              
825 0           $self->_snmp_ensure_is_connected();
826              
827 0           my @sessions = @{$self->{'_SNMP_SESSIONS'}};
  0            
828 0           my @errors = ();
829              
830 0           my $results = undef;
831              
832             # Attempt SNMP GET on each host listed .. first one that responds
833             # properly wins .. if none respond, throw an error.
834              
835             SNMP_GET_AGENT:
836 0           for my $s (@sessions) {
837              
838             # Ensure agent actually responded .. do not throw other errors
839             # for now as invalid OIDs will throw errors and we do not want
840             # the end user to have to catch those in parent code .. easy
841             # enough to look for the string constants that represent a
842             # missing OID condition - noSuchObject or noSuchInstance
843              
844 0           my $host = $s->hostname();
845 0           $self->debug("$host - attempting get_request()");
846              
847 0           $results = $s->get_request('-varbindlist' => \@oids);
848              
849 0 0         if (! defined $results) {
850              
851 0           my $error = $s->error();
852              
853 0 0         if ($error =~ /No response from/i) {
854 0           push(@errors, qq{$host - no response - } . join(', ', @oids)
855             . qq{ - $error});
856 0           $self->debug("$host - get_request failed - $error");
857             }
858             else {
859              
860             # If we have multiple hosts to potentially check,
861             # any error is recorded.
862              
863 0 0         if (scalar(@sessions) > 1) {
864 0           push(@errors, qq{$host - } . join(', ', @oids)
865             . qq{ - $error});
866 0           $self->debug("get_request() - $error");
867             }
868             }
869              
870             }
871             else {
872              
873 0           $self->{'_SNMP_POLLED_HOST'} = $host;
874 0           $self->debug("$host - get_request succeeded!");
875              
876 0 0         if ($self->{'_SNMP_DIE_ON_NO_SUCH'} == 1) {
877 0           my $nosuch = _ensure_defined_results($host, $results);
878 0 0         if (defined $nosuch) {
879 0           push(@errors, $nosuch);
880 0           $self->debug(
881             "$host - get_request returned noSuch* errors");
882             # Skip to next agent as we found an unsupported OID
883 0           next SNMP_GET_AGENT;
884             }
885             }
886              
887 0           last SNMP_GET_AGENT;
888              
889             }
890              
891             }
892              
893 0 0 0       if ( (scalar(@errors) > 0) && (scalar(@errors) == scalar(@sessions)) ) {
894 0           $self->nagios_exit(UNKNOWN, q{Net::SNMP get_request() failed: }
895             . join(', ', @errors));
896             }
897              
898 0           return $results;
899              
900             }
901              
902             =pod
903              
904             =head3 walk(@baseoids) - Perform an SNMP walk request
905              
906             Performs an SNMP walk on each passed in OID; uses the Net-SNMP
907             get_table() method for each base OID to ensure that the method will
908             work regardless of SNMP version in use. Returns results as
909             a hash reference where keys are the passed in base OIDs and the values are
910             references to the results of the Net::SNMP get_table calls.
911              
912             =cut
913              
914             sub walk {
915              
916 0     0 1   my $self = shift;
917 0           my @baseoids = @_;
918              
919 0           $self->_snmp_ensure_is_connected();
920              
921 0           my @sessions = @{$self->{'_SNMP_SESSIONS'}};
  0            
922              
923 0           my %results;
924 0           my @errors = ();
925              
926             # Attempt a walk on all sessions; first successful walk wins,
927             # throw an error if all sessions fail.
928              
929             SNMP_AGENT:
930 0           for my $s (@sessions) {
931              
932 0           my $successes = 0;
933              
934 0           my $host = $s->hostname();
935 0           $self->debug("$host - attempting get_table()");
936              
937             GET_TABLE:
938 0           for my $baseoid (@baseoids) {
939              
940             # Ensure agent actually responded .. do not throw other errors
941             # for now as invalid OIDs will throw errors and we do not want
942             # the end user to have to catch those in parent code .. easy
943             # enough to look for the string constants that represent a
944             # missing OID condition - noSuchObject or noSuchInstance
945              
946 0           my $result = $s->get_table($baseoid);
947              
948 0 0         if (! defined $result) {
949              
950 0           my $error = $s->error();
951              
952 0 0         if ($error =~ /No response from/i) {
953 0           push(@errors, qq{$host - $error});
954 0           $self->debug("get_table() - $baseoid - $error");
955 0           %results = ();
956             }
957             else {
958              
959             # If we have multiple hosts to potentially check,
960             # any error is recorded.
961              
962 1     1   18992 use Data::Dumper;
  1         10178  
  1         2135  
963 0           $self->debug(Dumper($result));
964              
965 0 0         if (scalar(@sessions) > 1) {
966 0           push(@errors, qq{$host - $baseoid - $error});
967 0           $self->debug("get_table() - $baseoid - $error");
968             }
969              
970             }
971              
972 0           next SNMP_AGENT;
973             }
974             else {
975 0           $self->debug("$host - get_table() succeeded for $baseoid");
976              
977 0 0         if ($self->{'_SNMP_DIE_ON_NO_SUCH'} == 1) {
978 0           my $error = _ensure_defined_results($host, $result);
979 0 0         if (defined $error) {
980 0           push(@errors, $error);
981 0           $self->debug(
982             "$host - get_table returned noSuch* errors");
983             # Skip to next agent as we found an unsupported OID
984 0           next SNMP_AGENT;
985             }
986              
987             }
988              
989 0           $results{$baseoid} = $result;
990 0           $successes++;
991              
992             }
993              
994             }
995              
996 0 0         if ($successes == scalar(@baseoids)) {
997 0           $self->debug("$host - walk succeeded for all OIDs");
998 0           last SNMP_AGENT;
999             }
1000              
1001             }
1002              
1003 0 0 0       if ( (scalar(@errors) > 0) && (scalar(@errors) == scalar(@sessions)) ) {
1004 0           $self->nagios_exit(UNKNOWN, q{Net::SNMP get_table() failed: }
1005             . join(', ', @errors));
1006             }
1007              
1008              
1009 0           return \%results;
1010             }
1011              
1012             sub get_deltas {
1013 0     0 1   my ($self, @oids) = @_;
1014              
1015 0           my $results = $self->get(@oids);
1016              
1017 0           for my $oid (keys %$results) {
1018 0           my $value = $results->{$oid};
1019 0           $self->debug("get_deltas(): get_value_for(value($oid, $value)");
1020 0           $results->{$oid} = $self->get_delta_for_value($oid, $value);
1021             }
1022              
1023 0           return $results;
1024              
1025             }
1026              
1027             sub get_delta_for_value {
1028 0     0 1   my ($self, $key, $current_value) = @_;
1029              
1030 0           my $cache_id = $self->get_cache_key_for($key);
1031 0           my ($previous_run_at, $cached_value) = $self->_get_from_cache($cache_id);
1032              
1033 0           my $interval = $self->opts->get('check-interval');
1034              
1035 0           my $delta_function_args = { 'previous_value' => $cached_value,
1036             'current_value' => $current_value,
1037             'interval' => $interval,
1038             'previous_run_at' => $previous_run_at,
1039             'cache_id' => $cache_id,
1040             'key' => $key };
1041              
1042 0 0         if ($self->opts->get('snmp-debug') == 1) {
1043 0           $self-> debug("get_delta_for_value($self, $delta_function_args");
1044 0           for my $v (keys %{ $delta_function_args }) {
  0            
1045 0           my $dv = '';
1046 0 0         $dv = $delta_function_args->{$v}
1047             if defined $delta_function_args->{$v};
1048 0           $self->debug(qq{get_delta_for_value: $v => '$dv'});
1049             }
1050             }
1051              
1052             # Call delta function; could be built in, could be user-provided
1053 0           my $cdf = $self->_get_compute_delta_function();
1054              
1055 0           my ( $new_cache_value, $delta ) = &{ $cdf }($self, $delta_function_args);
  0            
1056              
1057 0           $self->debug(qq{get_delta_for_value: }
1058             . qq{new_cache_value:$new_cache_value delta:$delta});
1059              
1060 0           $self->_store_in_cache($cache_id, $new_cache_value);
1061              
1062 0           return $delta;
1063              
1064             }
1065              
1066             =pod
1067              
1068             =head3 delta_compute_function
1069              
1070             Default delta computation function; used if user does not provide a
1071             delta compute function. This function will do the following:
1072             * If no value was in the cache previous to this call, it will return
1073             -0.
1074             * If the current value is less than the cached value, the function will
1075             return -0 and treat the case as a counter wrap.
1076             * If neither of the above are true, the function will return the difference
1077             between the current and previous values and store the current value in
1078             the cache.
1079              
1080             Is this overly-simplistic? Yes :), and it is designed to be replaced by
1081             your function that does a delta in a much more intelligent way.
1082              
1083             To replace this function with yours, subclass Nagios::Plugin::SNMP's
1084             default delta_compute_function method OR pass in a reference to a function
1085             via the 'process_deltas' hash passed to new(), e.g.
1086              
1087             my $plugin = Nagios::Plugin::SNMP->new({
1088             'delta_compute_function' => \&my_delta_function,
1089             ...
1090             });
1091              
1092             The delta computation function you create must accept the following
1093             arguments:
1094             * Reference to plugin instance (commonly called $self within a method)
1095             * Hash reference with the following key value pairs:
1096             * previous_value: Previous value (from the cache)
1097             * current_value: Current value (from the user or the remote
1098             SNMP agent)
1099             * interval: How long check interval is in seconds
1100             * previous_run_at: Unix timestamp representing the previous
1101             time a value was stored
1102             * key: Unique sub-key associated with this data, e.g.
1103             the OID for the data.
1104              
1105             The function must return (as a 2-element list):
1106             * Value to store in the cache
1107             * Delta between the two values passed to the function
1108              
1109             Note that this means your function can put any additional information
1110             in the value (and therefore cache) it would like as the function has
1111             total control over computing the delta between the previous and current
1112             values and control over what gets stored in the cache between runs of
1113             the plugin.
1114              
1115             Example:
1116              
1117             sub my_better_function {
1118             my ($self, $args_ref) = @_;
1119            
1120             my $previous_value = $args->{'previous_value'};
1121             my $current_value = $args->{'current_value'};
1122             my $interval = $args->{'interval'};
1123             my $previous_run_at = $args->{'previous_run_at'};
1124             my $key = $args->{'key'};
1125              
1126             my ($value_to_store, $delta_value) = ();
1127              
1128             # ... code to compute ...
1129              
1130             return ($value_to_store, $delta_value);
1131             }
1132              
1133             my $plugin = Nagios::Plugin::SNMP->new(
1134             'shortname' => 'FOO',
1135             'usage' => $usage,
1136             'process_deltas' => {
1137             'cache' => {
1138             'type' => 'memcache',
1139             ...
1140             }
1141             'delta_compute_function' => \&my_delta_function
1142             }
1143             );
1144              
1145             =cut
1146              
1147             sub delta_compute_function {
1148 0     0 1   my ($self, $args) = @_;
1149              
1150 0           my $previous_value = $args->{'previous_value'};
1151 0           my $current_value = $args->{'current_value'};
1152 0           my $interval = $args->{'interval'};
1153 0           my $previous_run_at = $args->{'previous_run_at'};
1154 0           my $key = $args->{'key'};
1155              
1156 0 0         return ($current_value, q{-0}) if ! defined $previous_value;
1157 0 0         return (q{-0}, q{-0}) if $current_value < $previous_value;
1158 0           return ($current_value, ($current_value - $previous_value));
1159              
1160             }
1161              
1162             =pod
1163              
1164             =head2 _get_compute_delta_function()
1165              
1166             Return user provided delta function reference if passed in or default
1167             delta compute function. Validation of function is done in new().
1168              
1169             =cut
1170              
1171             sub _get_compute_delta_function {
1172              
1173 0     0     my $self = shift;
1174              
1175 0 0         if (exists $self->{'_SNMP_PROCESS_DELTAS'}->{'callback'}) {
1176 0           $self->debug("Using custom delta compute function");
1177 0           return $self->{'_SNMP_PROCESS_DELTAS'}->{'callback'};
1178             }
1179             else {
1180 0           $self->debug("Using built-in delta compute function");
1181 0           return \&delta_compute_function;
1182             }
1183              
1184             }
1185              
1186             sub _snmp_ensure_is_connected {
1187              
1188 0     0     my $self = shift;
1189              
1190 0 0 0       if ( (! defined( $self->{'_SNMP_SESSIONS'}) ) ||
  0            
1191             ( scalar(@{$self->{'_SNMP_SESSIONS'}}) == 0 ) ) {
1192              
1193 0           $self->connect();
1194              
1195             }
1196              
1197             }
1198              
1199             sub close {
1200              
1201 0     0 0   my $self = shift;
1202              
1203 0 0         if (defined $self->{'_SNMP_SESSIONS'}) {
1204              
1205 0           my @sessions = @{$self->{'_SNMP_SESSIONS'}};
  0            
1206              
1207 0           for my $s (@sessions) {
1208 0           $s->close();
1209             }
1210              
1211             # Ensure we release Net::SNMP memory
1212 0           $self->{'_SNMP_SESSIONS'} = [];
1213              
1214             }
1215              
1216 0           return 1;
1217              
1218             }
1219              
1220             # Overloaded methods
1221              
1222             sub getopts {
1223              
1224 0     0 1   my $self = shift;
1225              
1226 0           $self->SUPER::getopts();
1227              
1228             # Now validate our options
1229 0           $self->_snmp_validate_opts();
1230              
1231             # If user requested delta processing to be done ('process_deltas' hash
1232             # ref passed to new()), start a cache instance. All validation of cache
1233             # options is done in new via _setup_delta_cache_options.
1234 0 0         $self->_initialize_delta_cache()
1235             if exists $self->{'_SNMP_PROCESS_DELTAS'};
1236              
1237             # Have to show this debug message here, can't do it in new() as
1238             # we haven't triggered Nagios::Plugin to do option processing yet.
1239 0 0         if (defined $self->{'_SNMP_PROCESS_DELTAS'}) {
1240              
1241 0           my $co = $self->{'_SNMP_PROCESS_DELTAS'};
1242              
1243             }
1244              
1245             # Start a plugin-level timer if we have one;
1246             # we will silently ignore the request if the
1247             # timeout option is not specified by the user.
1248 0           $self->start_timer();
1249              
1250             }
1251              
1252             =pod
1253              
1254             =head3 get_sys_info()
1255              
1256             my ($descr, $object_id) = $plugin->get_sys_info();
1257              
1258             Returns the sysDescr.0 and sysObjectId.0 OIDs from the remote
1259             agent, the sysObjectId.0 OID is translated to an OS family; string
1260             returned will be one of:
1261              
1262             * hpux
1263             * sunos4
1264             * solaris
1265             * osf
1266             * ultrix
1267             * hpux10
1268             * netbsd1
1269             * freebsd
1270             * irix
1271             * linux
1272             * bsdi
1273             * openbsd
1274             * win32
1275             * hpux11
1276             * unknown
1277              
1278             sysDescr.0 is a free-text description containing more specific
1279             information on the OS being queried.
1280              
1281             =cut
1282              
1283             sub get_sys_info {
1284              
1285 0     0 1   my $self = shift;
1286              
1287 0           my %oids = qw(
1288             sysdescr .1.3.6.1.2.1.1.1.0
1289             sysobjectid .1.3.6.1.2.1.1.2.0
1290             );
1291              
1292 0           my $result = $self->get(values %oids);
1293              
1294 0           return ($OS_TYPES{$result->{$oids{'sysobjectid'}}},
1295             $result->{$oids{'sysdescr'}});
1296              
1297             }
1298              
1299             sub _ensure_defined_results {
1300 0     0     my ($host, $results_hash_ref) = @_;
1301              
1302 0           my @errors = ();
1303              
1304 0           for my $oid (sort keys %{$results_hash_ref}) {
  0            
1305 0           my $value = $results_hash_ref->{$oid};
1306 0 0         if ($value =~ m/nosuch/msi) {
1307 0           push(@errors, "${host}:${oid} $value");
1308             }
1309             }
1310              
1311 0 0         return (scalar(@errors) == 0) ? undef : join(', ', @errors);
1312              
1313             }
1314              
1315             sub debug {
1316 0     0 0   my $self = shift;
1317 0 0         return unless $self->opts->get('snmp-debug') == 1;
1318              
1319 0           my $msg = shift;
1320 0           print STDERR scalar(localtime()) . qq{: $msg\n};
1321              
1322             }
1323              
1324             =pod
1325              
1326             =head1 AUTHORS
1327              
1328             * Max Schubert (maxschube@cpan.org)
1329             * Ryan Richins
1330             * Shaofeng Yang
1331              
1332             =head1 Special Thanks
1333              
1334             Special thanks to my teammates Ryan Richins and Shaofeng Yang at Comcast
1335             for their significant contributions to this module and to my managers
1336             Jason Livingood and Mike Fischer at Comcast for allowing our team to
1337             contribute code we have created or modified at work back to the open
1338             source community. If you live in the northern Virginia area and are
1339             a talented developer / systems administrator, Comcast is hiring :).
1340              
1341             =cut
1342              
1343             1;