package POE::Component::SNMP; $VERSION = '0.97'; use strict; use Net::SNMP (); use POE::Session; use POE::Component::SNMP::Dispatcher; use Carp; sub create { my $class = shift; my %arg = @_; my $alias = delete $arg{alias} || delete $arg{-alias} || 'snmp'; # die unless we get a hostname unless (exists $arg{hostname} || exists $arg{-hostname}) { croak "-hostname parameter required"; } # bitch unless we get a community unless (exists $arg{community} || exists $arg{-community}) { carp "using default value 'public' for missing -community parameter"; $arg{-community} = 'public'; } my ($session, $error); # each session binds to a different local port/socket. This # do..while loop catches any potential port conflicts. do { ($session, $error) = Net::SNMP->session( -nonblocking => 1, -localport => int(rand(65536 - 1025) + 1025 ), %arg, ); } while ($error =~ /^bind..:/); croak $error unless $session; POE::Session->create( inline_states => { _start => \&start_snmp_session, # _stop => \&end_snmp_session, finish => \&close_snmp_session, get => \&snmp_get, getnext => \&snmp_getnext, walk => \&snmp_walk, getbulk => \&snmp_getbulk, trap => \&snmp_trap, inform => \&snmp_inform, set => \&snmp_set, errmsg => \&snmp_errmsg, }, args => [ $alias, # component alias $session, # Net::SNMP session $error, # Net::SNMP error ], ); } sub start_snmp_session { my ($kernel, $heap, $alias, $session, $error) = @_[KERNEL, HEAP, ARG0..$#_]; # make sure we aren't duplicating component aliases! # if ($kernel->alias_set($alias)) { if (eval { $kernel->alias_resolve($alias) }) { local $Carp::CarpLevel = 4; # munge up to the right level of code croak "A ", __PACKAGE__, " instance called '$alias' already exists!"; } $kernel->alias_set($alias); $heap->{comp_alias} = $alias; # component alias $heap->{snmp_session} = $session; # Net::SNMP session $heap->{postback_args} = [ $alias, $session->hostname ]; } sub close_snmp_session { my ($kernel, $heap) = @_[KERNEL, HEAP]; my $snmp_session = delete $heap->{snmp_session}; $snmp_session->close; my $alias = delete $heap->{comp_alias}; $kernel->alias_remove($alias); } sub end_snmp_session { my ($kernel, $heap) = @_[KERNEL, HEAP]; } sub snmp_get { snmp_request( get_request => @_ ) } sub snmp_getnext { snmp_request( get_next_request => @_ ) } sub snmp_walk { snmp_request( get_table => @_ ) } sub snmp_getbulk { snmp_request( get_bulk_request => @_ ) } sub snmp_inform { snmp_request( inform_request => @_ ) } sub snmp_set { snmp_request( set_request => @_ ) } sub snmp_request { # first parameter is the Net::SNMP method to call my $method = shift; # then standard POE args my ($kernel, $heap, $sender, $state_name, @snmp_args) = @_[KERNEL, HEAP, SENDER, ARG0..$#_]; # extract the PoCo::SNMP request method called, for diagnostics # 'POE::Component::SNMP::snmp_get' => 'get' my $action = (caller(1))[3]; $action =~ s/.*_//; # do this before the 'set' logic to return an original copy of # @snmp_args to the callback. my @postback_args = (@{$heap->{postback_args}}, $action, @snmp_args); if ($action eq 'set') { # string => numeric constant processing @snmp_args = _dwim_set_request_args(@snmp_args); } my $postback = $sender->postback($state_name => @postback_args); my $ok = $heap->{snmp_session}->$method( @snmp_args, -callback => sub { $postback->( defined ($_[0]->var_bind_list) ? $_[0]->var_bind_list : $_[0]->error, # I know this is redundant, it's for # compatibility. To be removed eventually. $_[0]->error ) }, ); $kernel->post( $sender => $state_name => \@postback_args, [ defined ($heap->{snmp_session}->var_bind_list) ? $heap->{snmp_session}->var_bind_list : $heap->{snmp_session}->error, # I know this is redundant, it's for compatibility. To be removed eventually. $heap->{snmp_session}->error ] ) unless $ok; } sub snmp_trap { my ($kernel, $heap, @snmp_args) = @_[KERNEL, HEAP, ARG0..$#_]; my $ok = $heap->{snmp_session}->trap( @snmp_args ); # carp $heap->{snmp_session}->error unless $ok; $ok; # if we get call()'d, pass on the return value } # invoke with: $error = $kernel->call( $alias => error ); sub snmp_errmsg { $_[HEAP]{snmp_session}->error } # change string constant like 'OCTET_STRING' to a number # For a set request, the 2nd item of the varbindlist should be a # string constant indicating the value type. This block does a lookup # of the numeric equivalent and replaces it in the parameter list. sub _dwim_set_request_args { my %snmp_args = @_; # extract the varbindlist from args my $vbl = exists $snmp_args{varbindlist} ? $snmp_args{varbindlist} : $snmp_args{-varbindlist}; # make $type refer to the string constant my $type = ref($vbl) eq 'ARRAY' ? \$vbl->[1] : \'foo'; # if Net::SNMP::Message knows about it, use it to replace the # string with its numeric equivalent, e.g. 'OCTET_STRING' => 4 if ( Net::SNMP::Message->can($$type) ) { $$type = Net::SNMP::Message->${$type}(); } %snmp_args; # flatten back to a simple list. } 1; __END__ =pod =head1 NAME POE::Component::SNMP - L<POE> interface to L<Net::SNMP> =head1 SYNOPSIS # this script is included in the distribution as eg/snmp_sample.pl use POE qw/Component::SNMP/; my %system = ( sysUptime => '.1.3.6.1.2.1.1.3.0', sysName => '.1.3.6.1.2.1.1.5.0', sysLocation => '.1.3.6.1.2.1.1.6.0', ); my @oids = values %system; my $base_oid = '.1.3.6.1.2.1.1'; # system.* POE::Session->create( inline_states => { _start => \&_start, snmp_handler => \&snmp_handler, } ); sub _start { my ($kernel, $heap) = @_[KERNEL, HEAP]; POE::Component::SNMP->create( alias => 'snmp', # same as default hostname => 'localhost', community => 'public', timeout => 3, ); $kernel->post( snmp => get => snmp_handler => -varbindlist => \@oids ); # ... or maybe even ... $kernel->post( snmp => walk => snmp_handler => -baseoid => $base_oid ); $heap->{pending} = 2; } sub snmp_handler { my ($kernel, $heap, $request, $response) = @_[KERNEL, HEAP, ARG0, ARG1]; my ($alias, $host, $cmd, @args) = @$request; my ($results) = @$response; if (ref $results) { print "$host SNMP config ($cmd):\n"; print "sysName: $results->{$system{sysName}}\n"; print "sysUptime: $results->{$system{sysUptime}}\n"; print "sysLocation: $results->{$system{sysLocation}}\n"; } else { print "$host SNMP error ($cmd => @args):\n$results\n"; } if (--$heap->{pending} == 0) { $kernel->post( $alias => 'finish' ); } } $poe_kernel->run(); =head1 DESCRIPTION POE::Component::SNMP is a L<POE>-ized wrapper around the L<Net::SNMP> module written by David M. Town. Most of its arguments aren't even evaluated by POE, except for C<-alias>, as described below. =head1 CREATING SNMP COMPONENTS =over 4 =item B<create> - create an SNMP session POE::Component::SNMP->create( hostname => $hostname, # required [alias => $alias, ] # default 'snmp' [community => $community,] # default 'public', with a warning [version => $version, ] # default '1', SNMPv1 [timeout => $timeout, ] # default 5.0 [retries => $retries, ] # default 1 [debug => $debug, ] # default 0 [ ... any other arguments Net::SNMP recognizes ... ] ); C<create()> passes all of its arguments to the constructor for a L<Net::SNMP> object untouched with the exception of C<-alias>. See L<Net::SNMP/METHODS> on C<session()> for details. The constructor supports either of the following two parameter naming styles: $object->method(-parameter => $value); $object->method( parameter => $value); C<-hostname> is required. C<-community> defaults to 'public', but the module will emit a runtime warning if the community is not explicitly specified. C<-alias> is not required unless you want to query more than one host. See L</Concurrency>, below. =back =head2 Concurrency In order to access multiple SNMP hosts simultaneously, you must create a separate instance of the component for each host, by giving each component a different C<-alias> parameter in the constructor. The C<-alias> and C<-hostname> parameters, as well as additional request-specific data, are passed back to callback events, as described in L</CALLBACKS> below, so the callback can determine what context the current response (or timeout) is related to. Note: It is a fatal runtime error to attempt to create more than one SNMP session with the same C<-alias>. =head2 Sockets By default, L<Net::SNMP> creates a single socket per I<network interface>. Since L<POE> can only watch one connection per socket at a time, this creates a conflict if you want to contact more than one remote host simultaneously. The workaround used by the module is to create each socket using a different randomly generated value for the C<-localport> parameter, specifying a unique local UDP port for each host. This could potentially interfere with remote communications if your local firewall policy requires a specific source port for outgoing SNMP requests (as noted by David Town, the author of L<Net::SNMP>). In this situation, you can supply an explicit C<-localport> argument to the constructor, but remember that every active session requires its own I<unique> local port per session/host, per interface. =head1 REQUESTS Most of the events accept a list of arguments which are passed directly to a L<Net::SNMP> session. See L<Net::SNMP/METHODS> for more information on these arguments. Requests take the form: $poe_kernel->post( $session_alias => $request => $callback_state => @snmp_args ); See the SYNOPSIS for specific examples. =over 4 =item B<get> See L<Net::SNMP/METHODS> on C<get_request()>. =item B<getnext> See L<Net::SNMP/METHODS> on C<get_next_request()>. =item B<getbulk> See L<Net::SNMP/METHODS> on C<get_bulk_request()>. =item B<walk> See L<Net::SNMP/METHODS> on C<get_table()>. =item B<inform> See L<Net::SNMP/METHODS> on C<inform_request()>. =item B<set> See L<Net::SNMP/METHODS> on C<set_request()>. =item B<trap> $kernel->post( snmp => trap => @snmp_args ); # or, even better: my $status = $kernel->call( snmp => trap => @snmp_args ); Send a SNMPv1 trap message. See L<Net::SNMP/METHODS> on C<trap()>. This method differs from the others in that it does *not* take a state name as a callback parameter. If the method is invoked with C<$kernel-E<gt>call()>, the return value is that of L<C<trap()>|Net::SNMP/METHODS>. A false value indicates an error, and the error message can be retrieved using C<errmsg>, below. =item B<errmsg> my $last_snmp_error_message = $kernel->call( snmp => 'errmsg' ); Retrieves the last SNMP error message, if any, from the specified SNMP session. =item B<finish> $kernel->post( snmp => 'finish' ); Shut down the SNMP component. Cancels any pending requests and closes the session. =back =head1 CALLBACKS When a request either receives a response or times out, the supplied callback event (a POE event name defined in the session that called the SNMP component) is invoked. The callback's C<$_[ARG0]> parameter is an array reference containing the request information: the component alias, hostname, the method called (e.g. 'get'), and parameters supplied to the request. The callback's C<$_[ARG1]> parameter is an array reference containing the response information: I<either> a hash reference containing response data I<or> a scalar error message string. B<Note:> This is a change from previous versions of the module! For compatibility, any errors are still returned in C<$_[ARG1][1]>, but this I<will> go away. =head1 SEE ALSO Net::SNMP POE =head1 AUTHOR Adopted and maintained by Rob Bloodgood E<lt>rob@exitexchange.comE<gt> Originally by Todd Caine E<lt>tcaine@eli.netE<gt> =head1 COPYRIGHT AND LICENSE Copyright 2004-2006 by Rob Bloodgood Copyright 2003 by Todd Caine This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut