File Coverage

blib/lib/Mojo/SNMP.pm
Criterion Covered Total %
statement 122 191 63.8
branch 47 92 51.0
condition 17 33 51.5
subroutine 21 33 63.6
pod 8 8 100.0
total 215 357 60.2


line stmt bran cond sub pod time code
1             package Mojo::SNMP;
2              
3             =head1 NAME
4              
5             Mojo::SNMP - Run SNMP requests with Mojo::IOLoop
6              
7             =head1 VERSION
8              
9             0.11
10              
11             =head1 SYNOPSIS
12              
13             use Mojo::SNMP;
14             my $snmp = Mojo::SNMP->new;
15             my @response;
16              
17             $snmp->on(response => sub {
18             my($snmp, $session, $args) = @_;
19             warn "Got response from $args->{hostname} on $args->{method}(@{$args->{request}})...\n";
20             push @response, $session->var_bind_list;
21             });
22              
23             $snmp->defaults({
24             community => 'public', # v1, v2c
25             username => 'foo', # v3
26             version => 'v2c', # v1, v2c or v3
27             });
28              
29             $snmp->prepare('127.0.0.1', get_next => ['1.3.6.1.2.1.1.3.0']);
30             $snmp->prepare('localhost', { version => 'v3' }, get => ['1.3.6.1.2.1.1.3.0']);
31              
32             # start the IOLoop unless it is already running
33             $snmp->wait unless $snmp->ioloop->is_running;
34              
35             =head1 DESCRIPTION
36              
37             You should use this module if you need to fetch data from many SNMP servers
38             really fast. The module does its best to not get in your way, but rather
39             provide a simple API which allow you to extract information from multiple
40             servers at the same time.
41              
42             This module use L and L to fetch data from hosts
43             asynchronous. It does this by using a custom dispatcher,
44             L, which attach the sockets created by L
45             directly into the ioloop reactor.
46              
47             If you want greater speed, you should check out L and make sure
48             L is able to load.
49              
50             L is supposed to be a replacement for a module I wrote earlier,
51             called L. Reason for the rewrite is that I'm using the
52             framework L which includes an awesome IO loop which allow me to
53             do cool stuff inside my web server.
54              
55             =head1 CUSTOM SNMP REQUEST METHODS
56              
57             L provide methods to retrieve data from the SNMP agent, such as
58             L. It is possible to add custom methods if
59             you find yourself doing the same complicated logic over and over again.
60             Such methods can be added using L.
61              
62             There are two custom methods bundled to this package:
63              
64             =over 4
65              
66             =item * bulk_walk
67              
68             This method will run C until it receives an oid which does
69             not match the base OID. maxrepetitions is set to 10 by default, but could be
70             overrided by maxrepetitions inside C<%args>.
71              
72             Example:
73              
74             $self->prepare('192.168.0.1' => { maxrepetitions => 25 }, bulk_walk => [$oid, ...]);
75              
76             =item * walk
77              
78             This method will run C until the next oid retrieved does
79             not match the base OID or if the tree is exhausted.
80              
81             =back
82              
83             =cut
84              
85 11     11   272717 use Mojo::Base 'Mojo::EventEmitter';
  11         89786  
  11         76  
86 11     11   30424 use Mojo::IOLoop;
  11         1366550  
  11         75  
87 11     11   6500 use Mojo::SNMP::Dispatcher;
  11         34  
  11         109  
88 11     11   15273 use Net::SNMP ();
  11         200911  
  11         319  
89 11     11   78 use Scalar::Util ();
  11         18  
  11         349  
90 11 50   11   53 use constant DEBUG => $ENV{MOJO_SNMP_DEBUG} ? 1 : 0;
  11         20  
  11         723  
91 11     11   55 use constant MAXREPETITIONS => 10;
  11         20  
  11         34265  
92              
93             our $VERSION = '0.11';
94              
95             my $DISPATCHER;
96             my @EXCLUDE_METHOD_ARGS = qw( maxrepetitions );
97             my %EXCLUDE = (
98             v1 => [qw( username authkey authpassword authprotocol privkey privpassword privprotocol )],
99             v2c => [qw( username authkey authpassword authprotocol privkey privpassword privprotocol )],
100             v3 => [qw( community )],
101             );
102              
103             my %SNMP_METHOD;
104             __PACKAGE__->add_custom_request_method(bulk_walk => \&_snmp_method_bulk_walk);
105             __PACKAGE__->add_custom_request_method(walk => \&_snmp_method_walk);
106              
107             $Net::SNMP::DISPATCHER = $Net::SNMP::DISPATCHER; # avoid warning
108              
109             =head1 EVENTS
110              
111             =head2 error
112              
113             $self->on(error => sub {
114             my($self, $str, $session, $args) = @_;
115             });
116              
117             Emitted on errors which may occur. C<$session> is set if the error is a result
118             of a L method, such as L.
119              
120             See L for C<$args> description.
121              
122             =head2 finish
123              
124             $self->on(finish => sub {
125             my $self = shift;
126             });
127              
128             Emitted when all hosts have completed.
129              
130             =head2 response
131              
132             $self->on(response => sub {
133             my($self, $session, $args) = @_;
134             });
135              
136             Called each time a host responds. The C<$session> is the current L
137             object. C<$args> is a hash ref with the arguments given to L, with
138             some additional information:
139              
140             {
141             method => $str, # get, get_next, ...
142             request => [$oid, ...],
143             # ...
144             }
145              
146             =head2 timeout
147              
148             $self->on(timeout => sub {
149             my $self = shift;
150             })
151              
152             Emitted if L has been running for more than L seconds.
153              
154             =head1 ATTRIBUTES
155              
156             =head2 concurrent
157              
158             How many hosts to fetch data from at once. Default is 20. (The default may
159             change in later versions)
160              
161             =head2 defaults
162              
163             This attribute holds a hash ref with default arguments which will be passed
164             on to L. User-submitted C<%args> will be merged with the
165             defaults before being submitted to L. C will filter out
166             and ignore arguments that don't work for the SNMP C.
167              
168             NOTE: SNMP version will default to "v2c".
169              
170             =head2 master_timeout
171              
172             How long to run in total before timeout. Note: This is NOT per host but for
173             the complete run. Default is 0, meaning run for as long as you have to.
174              
175             =head2 ioloop
176              
177             Holds an instance of L.
178              
179             =cut
180              
181             has concurrent => 20;
182             has defaults => sub { +{} };
183             has master_timeout => 0;
184             has ioloop => sub { Mojo::IOLoop->singleton };
185              
186             # these attributes are experimental and therefore not exposed. Let me know if
187             # you use them...
188             has _dispatcher => sub { $DISPATCHER ||= Mojo::SNMP::Dispatcher->new(ioloop => shift->ioloop) };
189              
190             =head1 METHODS
191              
192             =head2 add_custom_request_method
193              
194             $self->add_custom_request_method(name => sub {
195             my($session, %args) = @_;
196             # do custom stuff..
197             });
198              
199             This method can be used to add custom L request methods. See the
200             source code for an example on how to do "walk".
201              
202             NOTE: This method will also replace any method, meaning the code below will
203             call the custom callback instead of L.
204              
205             $self->add_custom_request_method(get_next => $custom_callback);
206              
207             =cut
208              
209             sub add_custom_request_method {
210 23     23 1 615 my ($class, $name, $cb) = @_;
211 23         57 $SNMP_METHOD{$name} = $cb;
212 23         47 $class;
213             }
214              
215             =head2 get
216              
217             $self->get($host, $args, \@oids, sub {
218             my($self, $err, $res) = @_;
219             # ...
220             });
221              
222             Will call the callback when data is retrieved, instead of emitting the
223             L event.
224              
225             =head2 get_bulk
226              
227             $self->get_bulk($host, $args, \@oids, sub {
228             my($self, $err, $res) = @_;
229             # ...
230             });
231              
232             Will call the callback when data is retrieved, instead of emitting the
233             L event. C<$args> is optional.
234              
235             =head2 get_next
236              
237             $self->get_next($host, $args, \@oids, sub {
238             my($self, $err, $res) = @_;
239             # ...
240             });
241              
242             Will call the callback when data is retrieved, instead of emitting the
243             L event. C<$args> is optional.
244              
245             =head2 prepare
246              
247             $self = $self->prepare($host, \%args, ...);
248             $self = $self->prepare(\@hosts, \%args, ...);
249             $self = $self->prepare(\@hosts, ...);
250             $self = $self->prepare('*' => ...);
251              
252             =over 4
253              
254             =item * $host
255              
256             This can either be an array ref or a single host. The "host" can be whatever
257             L can handle; generally a hostname or IP address.
258              
259             =item * \%args
260              
261             A hash ref of options which will be passed directly to L.
262             This argument is optional. See also L.
263              
264             =item * dot-dot-dot
265              
266             A list of key-value pairs of SNMP operations and bindlists which will be given
267             to L. The operations are the same as the method names available in
268             L, but without "_request" at end:
269              
270             get
271             get_next
272             set
273             get_bulk
274             inform
275             walk
276             bulk_walk
277             ...
278              
279             The special hostname "*" will apply the given operation to all previously
280             defined hosts.
281              
282             =back
283              
284             Examples:
285              
286             $self->prepare('192.168.0.1' => { version => 'v2c' }, get_next => [$oid, ...]);
287             $self->prepare('192.168.0.1' => { version => 'v3' }, get => [$oid, ...]);
288             $self->prepare(localhost => set => [ $oid => OCTET_STRING, $value, ... ]);
289             $self->prepare('*' => get => [ $oid ... ]);
290              
291             Note: To get the C constant and friends you need to do:
292              
293             use Net::SNMP ':asn1';
294              
295             =cut
296              
297             sub prepare {
298 18 100   18 1 20261 my $cb = ref $_[-1] eq 'CODE' ? pop : undef; # internal usage. might change
299 18         32 my $self = shift;
300 18 50       62 my $hosts = ref $_[0] eq 'ARRAY' ? shift : [shift];
301 18 100       53 my $args = ref $_[0] eq 'HASH' ? shift : {};
302 18         68 my %args = %$args;
303              
304 18 50 66     137 $hosts = [keys %{$self->{sessions} || {}}] if $hosts->[0] and $hosts->[0] eq '*';
  5 100       35  
305              
306 18   66     32 defined $args{$_} or $args{$_} = $self->defaults->{$_} for keys %{$self->defaults};
  18         62  
307 18   100     458 $args{version} = $self->_normalize_version($args{version} || '');
308 18         30 delete $args{$_} for @{$EXCLUDE{$args{version}}}, @EXCLUDE_METHOD_ARGS;
  18         141  
309 18         34 delete $args{stash};
310              
311             HOST:
312 18         38 for my $key (@$hosts) {
313 25         89 my ($host) = $key =~ /^([^|]+)/;
314 25         70 local $args{hostname} = $host;
315 25 100       88 my $key = $key eq $host ? $self->_calculate_pool_key(\%args) : $key;
316 25 50 66     142 $self->{sessions}{$key} ||= $self->_new_session(\%args) or next HOST;
317              
318 25         65 local @_ = @_;
319 25         77 while (@_) {
320 18         34 my $method = shift;
321 18 100       59 my $oid = ref $_[0] eq 'ARRAY' ? shift : [shift];
322 18         28 push @{$self->{queue}{$key}}, [$key, $method, $oid, $args, $cb];
  18         131  
323             }
324             }
325              
326 18   100     83 $self->{n_requests} ||= 0;
327              
328 18         67 for ($self->{n_requests} .. $self->concurrent - 1) {
329 9 100       58 my $queue = $self->_dequeue or last;
330 7         24 $self->_prepare_request($queue);
331             }
332              
333 18 50 66     165 $self->_setup if !$self->{_setup}++ and $self->ioloop->is_running;
334 18         318 $self;
335             }
336              
337             =head2 set
338              
339             $self->set($host, $args => [ $oid => OCTET_STRING, $value, ... ], sub {
340             my($self, $err, $res) = @_;
341             # ...
342             });
343              
344             Will call the callback when data is set, instead of emitting the
345             L event. C<$args> is optional.
346              
347             =head2 walk
348              
349             $self->walk($host, $args, \@oids, sub {
350             my($self, $err, $res) = @_;
351             # ...
352             });
353              
354             Will call the callback when data is retrieved, instead of emitting the
355             L event. C<$args> is optional.
356              
357             =head2 wait
358              
359             This is useful if you want to block your code: C starts the ioloop and
360             runs until L or L is reached.
361              
362             my $snmp = Mojo::SNMP->new;
363             $snmp->prepare(...)->wait; # blocks while retrieving data
364             # ... your program continues after the SNMP operations have finished.
365              
366             =cut
367              
368             sub wait {
369 0     0 1 0 my $self = shift;
370 0         0 my $ioloop = $self->ioloop;
371 0         0 my $stop;
372              
373             $stop = sub {
374 0     0   0 $_[0]->unsubscribe(finish => $stop);
375 0         0 $_[0]->unsubscribe(timeout => $stop);
376 0         0 $ioloop->stop;
377 0         0 undef $stop;
378 0         0 };
379              
380 0 0       0 $self->_setup unless $self->{_setup}++;
381 0         0 $self->once(finish => $stop);
382 0         0 $self->once(timeout => $stop);
383 0         0 $ioloop->start;
384 0         0 $self;
385             }
386              
387             for my $method (qw( get get_bulk get_next set walk )) {
388 1 50   1 1 1968 eval <<"HERE" or die $@;
  1 0   0 1 6  
  1 50   1 1 7  
  0 0   0 1 0  
  0 0   0 1 0  
  0         0  
  1         33  
  1         5  
  1         6  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
389             sub $method {
390             my(\$self, \$host) = (shift, shift);
391             my \$args = ref \$_[0] eq 'HASH' ? shift : {};
392             \$self->prepare(\$host, \$args, $method => \@_);
393             }
394             1;
395             HERE
396             }
397              
398             sub _calculate_pool_key {
399 13 100   13   29 join '|', map { defined $_[1]->{$_} ? $_[1]->{$_} : '' } qw( hostname version community username );
  52         184  
400             }
401              
402             sub _dequeue {
403 13     13   21 my $self = shift;
404 13 50       21 my $key = (keys %{$self->{queue} || {}})[0] or return;
  13 100       79  
405 10         42 return delete $self->{queue}{$key};
406             }
407              
408             sub _finish {
409 1     1   1 warn "[Mojo::SNMP] Finish\n" if DEBUG;
410 1         4 $_[0]->emit('finish');
411 1         13 $_[0]->{_setup} = 0;
412             }
413              
414             sub _new_session {
415 10     10   21 my ($self, $args) = @_;
416 10         81 my ($session, $error) = Net::SNMP->new(%$args, nonblocking => 1);
417              
418 10         24050 warn "[Mojo::SNMP] New session $args->{hostname}: ", ($error || 'OK'), "\n" if DEBUG;
419 10 50   0   39 Mojo::IOLoop->next_tick(sub { $self->emit(error => "$args->{hostname}: $error") }) if $error;
  0         0  
420 10         57 $session;
421             }
422              
423             sub _normalize_version {
424 18 100   18   138 $_[1] =~ /1/ ? 'v1' : $_[1] =~ /3/ ? 'v3' : 'v2c';
    100          
425             }
426              
427             sub _prepare_request {
428 12     12   1244 my ($self, $queue) = @_;
429 12         21 my $item = shift @$queue;
430              
431 12 100       35 unless ($item) {
432 4 100       13 $queue = $self->_dequeue or return;
433 3         7 $item = shift @$queue;
434             }
435              
436 11         30 my ($key, $method, $list, $args, $cb) = @$item;
437 11         22 my $session = $self->{sessions}{$key};
438 11         14 my ($error, $success);
439              
440             # dispatch to our mojo based dispatcher
441 11         43 $Net::SNMP::DISPATCHER = $self->_dispatcher;
442              
443 11 100       163 unless ($session->transport) {
444 7         45 warn "[Mojo::SNMP] <<< open connection\n" if DEBUG;
445 7 50       30 unless ($session->open) {
446             Mojo::IOLoop->next_tick(
447             sub {
448 0 0   0   0 return $self->$cb($session->error, undef) if $cb;
449 0         0 return $self->emit(error => $session->error, $session, $args);
450             },
451 0         0 );
452 0   0     0 return $self->{n_requests} || '0e0';
453             }
454             }
455              
456 11         18846 warn "[Mojo::SNMP] <<< $method $key @$list\n" if DEBUG;
457 11         40 Scalar::Util::weaken($self);
458 11   66     57 $method = $SNMP_METHOD{$method} || "$method\_request";
459             $success = $session->$method(
460             $method =~ /bulk/ ? (maxrepetitions => $args->{maxrepetitions} || MAXREPETITIONS) : (),
461             ref $method ? (%$args) : (),
462             varbindlist => $list,
463             callback => sub {
464 2     2   2208 my $session = shift;
465              
466             eval {
467 2         12 local @$args{qw( method request )} = @$item[1, 2];
468 2         5 $self->{n_requests}--;
469 2 50       12 if ($session->var_bind_list) {
470 0         0 warn "[Mojo::SNMP] >>> success: $method $key @$list\n" if DEBUG;
471 0 0       0 return $self->$cb('', $session) if $cb;
472 0         0 return $self->emit(response => $session, $args);
473             }
474             else {
475 2         12 warn "[Mojo::SNMP] >>> error: $method $key @{[$session->error]}\n" if DEBUG;
476 2 50       8 return $self->$cb($session->error, undef) if $cb;
477 2         11 return $self->emit(error => $session->error, $session, $args);
478             }
479 0         0 1;
480 2 50       5 } or do {
481 0         0 $self->emit(error => $@);
482             };
483 2         278 warn "[Mojo::SNMP] n_requests: $self->{n_requests}\n" if DEBUG;
484 2         8 $self->_prepare_request($queue);
485 2         3 warn "[Mojo::SNMP] n_requests: $self->{n_requests}\n" if DEBUG;
486 2 100       12 $self->_finish unless $self->{n_requests};
487             },
488 11 100 50     151 );
    100          
489              
490 11 50       157 return ++$self->{n_requests} if $success;
491 0         0 $self->emit(error => $session->error, $session);
492 0   0     0 return $self->{n_requests} || '0e0';
493             }
494              
495             sub _setup {
496 2     2   1690 my $self = shift;
497 2 50       9 my $timeout = $self->master_timeout or return;
498 2         16 my $tid;
499              
500 2         3 warn "[Mojo::SNMP] Timeout: $timeout\n" if DEBUG;
501 2         7 Scalar::Util::weaken($self);
502              
503             $tid = $self->ioloop->timer(
504             $timeout => sub {
505 2     2   2718 warn "[Mojo::SNMP] Timeout\n" if DEBUG;
506 2         9 $self->ioloop->remove($tid);
507 2         81 $self->emit('timeout');
508 2         82 $self->{_setup} = 0;
509             }
510 2         8 );
511             }
512              
513             sub _snmp_method_bulk_walk {
514 2     2   9 my ($session, %args) = @_;
515 2         6 my $base_oid = $args{varbindlist}[0];
516 2         4 my $last = $args{callback};
517 2   100     11 my $maxrepetitions = $args{maxrepetitions} || MAXREPETITIONS;
518 2         3 my ($callback, $end, %tree, %types);
519              
520             $end = sub {
521 0 0   0   0 $session->pdu->var_bind_list(\%tree, \%types) if %tree;
522 0         0 $session->$last;
523 0         0 $end = $callback = undef;
524 2         9 };
525              
526             $callback = sub {
527 0     0   0 my ($session) = @_;
528 0 0       0 my $res = $session->var_bind_list or return $end->();
529 0 0       0 my @sortres = $session->var_bind_names() or return $end->();
530 0         0 my $types = $session->var_bind_types;
531 0         0 my $next = $sortres[-1];
532              
533 0         0 for my $oid (@sortres) {
534 0 0 0     0 return $end->() if $types{$oid} or !Net::SNMP::oid_base_match($base_oid, $oid);
535 0         0 $types{$oid} = $types->{$oid};
536 0         0 $tree{$oid} = $res->{$oid};
537             }
538              
539 0 0       0 return $end->() unless $next;
540 0         0 return $session->get_bulk_request(maxrepetitions => $maxrepetitions, varbindlist => [$next], callback => $callback);
541 2         10 };
542              
543 2         11 $session->get_bulk_request(maxrepetitions => $maxrepetitions, varbindlist => [$base_oid], callback => $callback);
544             }
545              
546             sub _snmp_method_walk {
547 0     0     my ($session, %args) = @_;
548 0           my $base_oid = $args{varbindlist}[0];
549 0           my $last = $args{callback};
550 0           my ($callback, $end, %tree, %types);
551              
552             $end = sub {
553 0 0   0     $session->pdu->var_bind_list(\%tree, \%types) if %tree;
554 0           $session->$last;
555 0           $end = $callback = undef;
556 0           };
557              
558             $callback = sub {
559 0     0     my ($session) = @_;
560 0 0         my $res = $session->var_bind_list or return $end->();
561 0           my $types = $session->var_bind_types;
562 0           my @next;
563              
564 0           for my $oid (keys %$res) {
565 0 0 0       if (!$types{$oid} and Net::SNMP::oid_base_match($base_oid, $oid)) {
566 0           $types{$oid} = $types->{$oid};
567 0           $tree{$oid} = $res->{$oid};
568 0           push @next, $oid;
569             }
570             }
571              
572 0 0         return $end->() unless @next;
573 0           return $session->get_next_request(varbindlist => \@next, callback => $callback);
574 0           };
575              
576 0           $session->get_next_request(varbindlist => [$base_oid], callback => $callback);
577             }
578              
579             =head1 COPYRIGHT & LICENSE
580              
581             This library is free software. You can redistribute it and/or modify
582             it under the same terms as Perl itself.
583              
584             =head1 AUTHOR
585              
586             Jan Henning Thorsen - C
587              
588             Joshua Keroes - C
589              
590             Espen Tallaksen
591              
592             =cut
593              
594             1;