—————package
POE::Component::SNMP::Session;
use
Carp;
use
POE;
use
POE::Session;
# use Smart::Comments;
use
warnings;
use
strict;
# {{{ pod intro
=head1 NAME
POE::Component::SNMP::Session - Wrap Net-SNMP's SNMP::Session in POE
=head1 VERSION
Version 0.08
=cut
our
$VERSION
=
'0.08'
;
=head1 SYNOPSIS
This module wraps the SNMP module from the net-snmp project within
POE's non-blocking event loop, to perform asynchronous SNMP requests.
use POE qw/Component::SNMP::Session/;
POE::Component::SNMP::Session->create();
...
NOTE: this is NOT compatible with the L<Net::SNMP> module by David
Town. See L<POE::Component::SNMP> for an async interface to
L<Net::SNMP>.
=head1 CREATING SNMP COMPONENTS
=cut
# }}} pod intro
our
$DEBUG
= 0;
our
$DISPATCHER
;
*DEBUG_INFO
= \
*POE::Component::SNMP::Session::Dispatcher::DEBUG_INFO
;
# {{{ BEGIN
BEGIN
{
# Validate the creation of the Dispatcher object.
if
(!
defined
(
$DISPATCHER
= POE::Component::SNMP::Session::Dispatcher->instance)) {
die
(
'FATAL: Failed to create Dispatcher instance'
);
}
}
# }}} BEGIN
# {{{ create
=over 4
=item B<create> - create an SNMP session
The constructor takes the same arguments as the L<SNMP::Session>
module, with one addition.
=over 4
=item Alias
the Alias parameter specifies the POE session alias the component will
receive. If this parameter is not supplied, the default value is
'snmp'. Be careful of creating duplicate sessions! Depending on your
environment, POE might throw an error, or it might not. So don't do
that.
=item DestHost
This parameter is technically optional, and defaults to 'localhost',
but you I<really> should set it. Also, this parameter name is Case
Sensitive, so it must be supplied in mixed case as shown here.
=back
All other parameters are passed through to SNMP::Session untouched.
NOTE: SNMPv3 session creation blocks until authorization completes.
This means that if your DestHost doesn't respond, your program will
block for Timeout microseconds (default 1s). Also, if authentication
fails, the constructor will fail, so it is important to check the
return value of C<create()> in this case.
=back
=cut
sub
create {
my
$class
=
shift
;
my
@arg
=
@_
;
my
%arg
;
# = @_;
my
(
$alias
,
$hostname
);
(
$alias
,
@arg
) = _arg_scan(
alias
=>
@arg
);
$alias
||=
'snmp'
;
push
@arg
,
Alias
=>
$alias
;
# make sure we aren't duplicating component aliases!
if
( ! (
$POE::VERSION
<= 0.95 and POE::Kernel::ASSERT_DATA) and
defined
POE::Kernel->alias_resolve(
$alias
)
) {
local
$Carp::CarpLevel
= 4;
# munge up to the right level of code
"-"
x 40,
"\n"
;
croak
"A "
, __PACKAGE__,
" instance called '$alias' already exists!"
;
"-"
x 40,
"\n"
;
}
# allow -hostname for compatibility, and put it in the Desthost slot automatically
(
$hostname
,
@arg
) = _arg_scan(
desthost
=>
@arg
);
(
$hostname
,
@arg
) = _arg_scan(
hostname
=>
@arg
)
unless
$hostname
;
push
@arg
,
DestHost
=>
$hostname
if
$hostname
;
# catch version
my
$version
;
(
$version
,
@arg
) = _arg_scan(
version
=>
@arg
);
$version
= 2
if
$version
&&
$version
eq
'snmpv2c'
;
$version
= 3
if
$version
&&
$version
eq
'snmpv3'
;
$version
||= 1;
push
@arg
,
Version
=>
$version
;
%arg
=
@arg
;
# use YAML; warn Dump \%arg;
# die unless we get a hostname
unless
( (_arg_scan(
desthost
=>
%arg
))[0] ) {
croak
"desthost parameter required"
;
}
# make sure we have a dispatcher!
if
(!
defined
(
$DISPATCHER
= POE::Component::SNMP::Session::Dispatcher->instance)) {
die
(
'FATAL: Failed to create Dispatcher instance'
);
}
my
(
undef
,
@fd
) = SNMP::select_info();
@fd
= ()
unless
defined
$fd
[0];
#### @fd
my
(
$session
,
$error
);
{
# local $SNMP::debugging = 3;
(
$session
,
$error
) =
SNMP::Session->new(
%arg
);
}
warn
$error
unless
$session
;
warn
$!
unless
$session
;
warn
$@
unless
$session
;
return
unless
$session
;
my
(
undef
,
@new_fd
) = SNMP::select_info();
#### @new_fd
# grab the one from @new_fd that is NOT in @fd
my
%h
;
@h
{
@new_fd
} = ();
delete
@h
{
@fd
};
my
(
$fd
) =
keys
%h
;
DEBUG_INFO(
"chose fd $fd"
);
POE::Session->create(
inline_states
=> {
_start
=> \
&_start_snmp_session
,
_stop
=> \
&_end_snmp_session
,
finish
=> \
&_close_snmp_session
,
get
=> \
&_snmp_get
,
getnext
=> \
&_snmp_getnext
,
bulkwalk
=> \
&_snmp_bulkwalk
,
getbulk
=> \
&_snmp_getbulk
,
# getentries => \&_snmp_getentries,
# trap => \&_snmp_trap,
# trap2c => \&_snmp_trap2c,
# inform => \&_snmp_inform,
set
=> \
&_snmp_set
,
},
args
=> [
$session
,
$fd
,
$arg
{desthost}
],
);
}
# }}} create
# {{{ _start_snmp_session
sub
_start_snmp_session {
my
(
$kernel
,
$heap
,
$session
,
$fd
,
$hostname
) =
@_
[KERNEL, HEAP, ARG0..
$#_
];
my
$alias
=
$session
->{Alias};
$kernel
->alias_set(
$alias
);
# $heap->{comp_alias} = $alias; # component alias
$heap
->{snmp_session} =
$session
;
# SNMP::Session
$heap
->{postback_args} = [
$alias
,
$hostname
,
$session
,
];
$kernel
->call(
$DISPATCHER
->
_alias
=>
__listen
=>
$session
,
$fd
);
return
1;
}
# }}} _start_snmp_session
# {{{ _close_snmp_session
sub
_close_snmp_session {
my
(
$kernel
,
$session
,
$heap
) =
@_
[KERNEL, SESSION, HEAP];
my
$snmp_session
=
delete
$heap
->{snmp_session};
return
unless
defined
$snmp_session
;
if
(0 and
$snmp_session
->debug & 0x08) {
"debug: ["
, __LINE__,
"] "
, __PACKAGE__,
"::_close_snmp_session: calling __clear_pending\n"
;
}
# cancel all pending requests
my
$rv
=
$kernel
->call(
$DISPATCHER
->
_alias
=>
__clear_pending
=>
$snmp_session
);
# undef $session;
# remove our alias... since we have no more pending requests, we
# will go away now.
$kernel
->alias_remove(
$_
)
for
$kernel
->alias_list(
$session
);
# now the only thing keeping this session alive are any postback
# references that have yet to be delivered.
}
# }}} _close_snmp_session
# {{{ _end_snmp_session
sub
_end_snmp_session {
my
(
$kernel
,
$heap
) =
@_
[KERNEL, HEAP];
# delete $heap->{snmp_session};
my
$session
=
delete
$heap
->{snmp_session};
# WWW { sede => $session };
# $DISPATCHER->_clear_session($session) if $session;
# WWW $DISPATCHER;
# $heap->{snmp_session}->close;
}
# }}} _end_snmp_session
=head1 REQUESTS
The requests accept a list of arguments which are passed directly to a
C<SNMP::Session> object. See L<SNMP/SNMP::Session> for more
information on these arguments.
Requests take the form:
$poe_kernel->post( $component_alias => $request =>
$callback_state => @snmp_args );
The arguments are the component alias, the request type, a callback
state in the requesting session, and then any arguments you would pass
to the SNMP::Session method of the same name.
=over 4
=item get
$poe_kernel->post( snmp => get => $state =>
[ '.1.3.6.1.2.1.1.3.0' ],
# or
[ 'sysUptime.0' ],
# or
[ sysUptime => 0 ],
# or
[ 'sysUptime' ],
);
=item getnext
=item getbulk
=item bulkwalk
=item set
These are the request types the component knows about. Details on the
correct parameters for each request type are in the
L<SNMP/SNMP::Session> docs.
For sending traps, you should instantiate an SNMP::TrapSession object
directly.
=item finish
Shuts down the component instance (other SNMP sessions are
unaffected). Any requests that are still pending will have their
respective responses/timeouts delivered, but new requests will be
discarded.
=back
=cut
sub
_snmp_get { _snmp_request(
get
=>
@_
) }
sub
_snmp_getnext { _snmp_request(
getnext
=>
@_
) }
sub
_snmp_set { _snmp_request(
set
=>
@_
) }
sub
_snmp_getbulk { _snmp_request(
getbulk
=>
@_
) }
sub
_snmp_bulkwalk { _snmp_request(
bulkwalk
=>
@_
) }
# sub _snmp_getable { _snmp_request( gettable => @_ ) }
# {{{ _snmp_request
sub
_snmp_request {
# first parameter is the Net::SNMP method to call
my
$method
=
shift
;
# then standard POE args
my
(
$kernel
,
$heap
,
$sender
,
$target_state
,
@snmp_args
) =
@_
[KERNEL, HEAP, SENDER, ARG0..
$#_
];
my
$session
=
$heap
->{snmp_session};
# extract the PoCo::SNMP request method called, for diagnostics
# 'POE::Component::SNMP::snmp_get' => 'get'
my
$action
= (
caller
(1))[3];
$action
=~ s/POE::Component::SNMP::Session::_snmp_//;
my
(
@callback_args
,
$callback_args
);
(
$callback_args
,
@snmp_args
) = _arg_scan(
callback_args
=>
@snmp_args
);
my
$ok
= 1;
# if $callback_args is defined, we got a callback_args in the request.
if
(
defined
$callback_args
) {
if
(
ref
$callback_args
eq
'ARRAY'
) {
@callback_args
=
@$callback_args
;
}
else
{
$ok
= 0;
# $heap->{snmp_session}->_error("Argument to -callback_args must be an arrayref");
$session
->{ErrorStr} =
"Argument to -callback_args must be an arrayref"
;
@callback_args
= (
$callback_args
);
# stash the "bad" argument to return with the error
}
}
# 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
);
# $ok = 1;
if
(
$ok
) {
if
(
$method
eq
'set_request'
) {
# string => numeric constant processing
@snmp_args
= _dwim_set_request_args(
@snmp_args
);
}
# this $postback is a closure. it goes away after firing.
my
$postback
=
$sender
->postback(
$target_state
=>
@postback_args
);
$ok
=
$DISPATCHER
->_send_pdu(
$session
=>
$method
=> \
@snmp_args
,
$postback
, \
@callback_args
);
}
else
{
$kernel
->post(
$sender
=>
$target_state
=> \
@postback_args
,
[
$session
,
$session
->{ErrorStr},
@callback_args
]
);
}
return
$ok
;
}
# }}} _snmp_request
# {{{ _arg_scan
# scan an array for a key matching qw/ -key key Key KEY / and fetch
# the value. return the value and the remaining arg list minus the
# key/value pair.
sub
_arg_scan {
my
(
$key
,
@arg
) =
@_
;
my
$value
;
# scan the @arg for any keys that are callback args.
for
(0..
$#arg
) {
# this pattern match likes to complain
no
warnings;
if
(
$arg
[
$_
] =~ /^-?
$key
$/i) {
$value
=
$arg
[
$_
+ 1];
# splice out the key and value from @arg:
splice
@arg
,
$_
, 2;
}
}
(
$value
,
@arg
);
}
# }}} _arg_scan
# {{{ get
# =item get
# this is a subclassed get(). Automatically sends requests to POE but needs a state name as the first parameter.
# =cut
# sub get {
# my ($session) = @_;
# return $session->SUPER() unless defined $session->{_poe_destination};
# # sub yield {
# # my $self = shift;
# # $poe_kernel->post( $self->session => @_ );
# # }
# return POE::Kernel->call($session->{Alias} => get => @_);
# }
# }}} get
# {{{ pod finish
=head1 CALLBACK STATES
A callback state (a POE event) is invoked when the component either
receives a response or timeout. The event receives data in its
C<$_[ARG0]> and C<$_[ARG1]> parameters.
C<$_[ARG0]> is an array reference containing: the C<SNMP::Session> object
that the component is using, the alias of the component, and the
hostname (C<DestHost>) the component is communicating with.
C<$_[ARG0]> is an array reference containing: the response value.
If the response value is defined, it will be a SNMP::VarBindList
object containing the SNMP results.
If the response value is C<undef>, then any error message can be
accessed via the C<SNMP::Session> object as $session->{ErrorStr}.
See L<SNMP/SNMP::Session> for details.
=head1 AUTHOR
Rob Bloodgood, C<< <rdb at cpan.org> >>
=head1 CAVEATS
SNMPv3 connections automatically send a synchronous (blocking) request
to establish authorization. If the request times out (for example if
the agent is not responding), the entire program will block for
$timeout microseconds. YMMV, but for unreliable or slow connections,
you may want to try a smaller timeout value, so you receive a failure
more quickly.
=head1 BUGS
Please report any bugs or feature requests to
C<bug-snmp-session-poe at rt.cpan.org>, or through the web interface at
I will be notified, and then you'll automatically be notified of progress on
your bug as I make changes.
=head1 SUPPORT
You can find documentation for this module with the perldoc command.
perldoc POE::Component::SNMP::Session
You can also look for information at:
=over 4
=item * AnnoCPAN: Annotated CPAN documentation
=item * CPAN Ratings
=item * RT: CPAN's request tracker
=item * Search CPAN
=back
=head1 ACKNOWLEDGEMENTS
=head1 COPYRIGHT & LICENSE
Copyright 2007 Rob Bloodgood, all rights reserved.
This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.
=cut
# }}}
1;
# End of POE::Component::SNMP::Session
# vi:foldmethod=marker: