File Coverage

blib/lib/Mojo/SNMP.pm
Criterion Covered Total %
statement 122 194 62.8
branch 47 94 50.0
condition 17 33 51.5
subroutine 21 34 61.7
pod 9 9 100.0
total 216 364 59.3


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