The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
package POE::Component::NetSNMP::agent;

use 5.006;
use strict;
use warnings;

use parent qw< POE::Session >;

use Carp;
use List::MoreUtils qw< after >;
use NetSNMP::agent;
use POE;
use SNMP ();
use SNMP::ToolBox;


our $VERSION = "0.500";


use constant {
    TYPE            => 0,
    VALUE           => 1,

    HAVE_SORT_KEY_OID
                    => eval "use Sort::Key::OID 0.04 'oidsort'; 1" ? 1 : 0,
};


#
# spawn()
# -----
sub spawn {
    my $class = shift;
    croak "error: odd number of arguments" unless @_ % 2 == 0;

    my %defaults = (
        Name    => "perl",
        AgentX  => 0,
        Ping    => 10,
    );

    my %args = ( %defaults, @_ );

    my @poe_opts;
    push @poe_opts, options => { trace => 1, debug => 1, default => 1 }
        if $args{Debug};

    # check arguments
    carp "warning: errback '$args{Errback}' doesn't look like a POE event"
        if $args{Errback} and $args{Errback} !~ /^\w+$/;

    # create the POE session
    my $session = $class->create(
        heap => {
            args        => \%args,
            oid_tree    => {},
            ping_delay  => $args{Ping},
        },

        inline_states => {
            _start      => \&ev_start,
            _stop       => \&ev_stop,
            init        => \&ev_init,
            register    => \&ev_register,
            agent_check => \&ev_agent_check,

            tree_handler    => \&ev_tree_handler,
            add_oid_entry   => \&ev_add_oid_entry,
            add_oid_tree    => \&ev_add_oid_tree,
        },

        @poe_opts,
    );

    return $session
}


# ==============================================================================
# POE events
#


#
# ev_start()
# --------
sub ev_start {
    $_[KERNEL]->yield("init");
    $_[KERNEL]->alias_set( $_[HEAP]{args}{Alias} )
        if $_[HEAP]{args}{Alias};
}


#
# ev_stop()
# -------
sub ev_stop {
    $_[HEAP]{agent}->shutdown;
}


#
# ev_init()
# -------
sub ev_init {
    my $args = $_[HEAP]{args};
    my %opts;
    $opts{Name}   = $args->{Name};
    $opts{AgentX} = $args->{AgentX};
    $opts{Ports}  = $args->{Ports} if defined $args->{Ports};

    # create the NetSNMP sub-agent
    $_[HEAP]{agent} = NetSNMP::agent->new(%opts);

    # if auto-handle is requested, register our own OID tree handler
    $_[KERNEL]->yield(register => $args->{AutoHandle}, "tree_handler")
        if $args->{AutoHandle};
}


#
# ev_register()
# -----------
sub ev_register {
    my ($kernel, $heap, $sender, $oid, $callback)
        = @_[ KERNEL, HEAP, SENDER, ARG0, ARG1 ];
    my $args = $heap->{args};

    my $poe_wrapper;

    if (ref $callback) {
        # simpler & faster callback mechanism
        my @poe_params = @_[ 0 .. ARG0-1 ];
        $poe_wrapper = sub {
            @_ = ( @poe_params, [], [@_] );
            goto $callback
        };
    }
    else {
        # standard POE callback mechanism
        $poe_wrapper = $sender->callback($callback);
    }

    # create & register the NetSNMP sub-agent
    my $r = $heap->{agent}->register(
        $args->{Name}, $oid, $poe_wrapper);

    if (not $r) {
        $kernel->post($sender, $args->{Errback}, "register")
            if $args->{Errback};
        return
    }

    # manually call agent_check_and_process() once so it opens
    # the sockets to AgentX master
    $kernel->delay(agent_check => 0, "register");
}


#
# ev_agent_check()
# --------------
sub ev_agent_check {
    my ($kernel, $heap, $case) = @_[ KERNEL, HEAP, ARG0 ];

    $case ||= "";

    # schedule next check
    $kernel->delay(agent_check => $heap->{ping_delay}),

    # process the incoming data and invoke the callback
    SNMP::_check_timeout();
    $heap->{agent}->agent_check_and_process(0);

    if ($case eq "register") {
        # find the sockets used to communicate with AgentX master..
        my ($block, $to_sec, $to_usec, @fd_set)
            = SNMP::_get_select_info();

        # ... and let POE kernel handle them
        for my $fd (@fd_set) {
            # create a file handle from the given file descriptor
            open my $fh, "+<&=", $fd;

            # first unregister the given file handles from
            # POE::Kernel, in case some were already registered,
            # then register them, with this event as callback
            $kernel->select_read($fh);
            $kernel->select_read($fh, "agent_check");
        }
    }
}


#
# ev_tree_handler()
# ---------------
sub ev_tree_handler {
    my ($kernel, $heap, $args) = @_[ KERNEL, HEAP, ARG1 ];
    my ($handler, $reg_info, $request_info, $requests) = @$args;
    my $oid_tree = $heap->{oid_tree};
    my $oid_list = $heap->{oid_list};

    # the rest of the code works like a classic NetSNMP::agent callback
    my $mode = $request_info->getMode;

    for (my $request = $requests; $request; $request = $request->next) {
        my $oid = $request->getOID->as_oid;

        if ($mode == MODE_GET) {
            if (exists $oid_tree->{$oid}) {
                # /!\ no intermediate vars. see comment at end
                $request->setValue(
                    $oid_tree->{$oid}[TYPE],
                    $oid_tree->{$oid}[VALUE]
                );
            }
            else {
                $request->setError($request_info, SNMP_ERR_NOSUCHNAME);
                next
            }
        }
        elsif ($mode == MODE_GETNEXT) {
            # find the OID after the requested one
            my $next_oid = find_next_oid($oid_list, $oid);

            if (exists $oid_tree->{$next_oid}) {
                # /!\ no intermediate vars. see comment at end
                $request->setOID($next_oid);
                $request->setValue(
                    $oid_tree->{$next_oid}[TYPE],
                    $oid_tree->{$next_oid}[VALUE]
                );
            }
            else {
                $request->setError($request_info, SNMP_ERR_NOSUCHNAME);
                next
            }
        }
        else {
            $request->setError($request_info, SNMP_ERR_GENERR);
            next
        }
    }
}


#
# ev_add_oid_entry()
# ----------------
sub ev_add_oid_entry {
    my ($kernel, $heap, $oid, $type, $value)
        = @_[ KERNEL, HEAP, ARG0, ARG1, ARG2 ];

    my $oid_tree = $heap->{oid_tree};

    # make sure that the OID start with a dot
    $oid = ".$oid" unless index($oid, ".") == 0;

    # add the given entry to the tree
    $oid_tree->{$oid} = [ $type, $value ];

    # calculate the sorted list of OID entries
    @{ $heap->{oid_list} } = HAVE_SORT_KEY_OID ?
        oidsort(keys %$oid_tree) : sort by_oid keys %$oid_tree;
}


#
# ev_add_oid_tree()
# ---------------
sub ev_add_oid_tree {
    my ($kernel, $heap, $new_tree) = @_[ KERNEL, HEAP, ARG0 ];

    my $oid_tree = $heap->{oid_tree};

    # make sure that the OIDs start with a dot
    my @oids = map { index($_, ".") == 0 ? $_ : ".$_" } keys %$new_tree;

    # add the given entries to the tree
    @{$oid_tree}{@oids} = values %$new_tree;

    # calculate the sorted list of OID entries
    @{ $heap->{oid_list} } = HAVE_SORT_KEY_OID ?
        oidsort(keys %$oid_tree) : sort by_oid keys %$oid_tree;
}


# ==============================================================================
# Methods
#


#
# new()
# ---
sub new {
    my $class = shift;
    croak "error: odd number of arguments" unless @_ % 2 == 0;

    my %args = @_;
    my $update_handlers = delete $args{AutoUpdate};
    carp "warning: no OID tree update handlers defined"
        unless $update_handlers;

    # instanciate ourself
    my $self = POE::Component::NetSNMP::agent->spawn(%args);

    # create a heap reserved for the update handlers
    $self->[0]{update_handlers_heap} = {};

    # create the states for wrapping around the update handlers
    my %state;
    for my $def (@$update_handlers) {
        my ($coderef, $delay) = @$def;
        my $name = "wrap_sub_$coderef";

        $state{$name} = sub {
            $_[KERNEL]->delay($name, $delay);
            eval { $coderef->($self) };
            warn $@ if $@;
        };
    }

    # create the additional session to execute the update handlers
    POE::Session->create(
        heap => {
            agent   => $self,
        },

        args => [ keys %state ],

        inline_states => {
            _start => sub {
                $_[KERNEL]->alias_set("main");
                $_[KERNEL]->yield($_) for @_[ARG0 .. $#_];
            },
            %state,
        },
    );

    return $self
}


#
# run()
# ---
sub run {
    POE::Kernel->run;
}


#
# heap()
# ----
sub heap {
    $_[0][0]{update_handlers_heap}
}


#
# register()
# --------
sub register {
    my ($self, $oid, $callback) = @_;

    # check arguments
    croak "error: no OID defined"       unless defined $oid;
    croak "error: no callback defined"  unless defined $callback;
    croak "error: callback must be a coderef"
        unless ref $callback and ref $callback eq "CODE";

    # register the given OID and callback
    POE::Kernel->post($self, register => $oid, $callback);

    return $self
}


#
# add_oid_entry()
# -------------
sub add_oid_entry {
    my ($self, $oid, $type, $value) = @_;

    # check arguments
    croak "error: no OID defined"       unless defined $oid;
    croak "error: no type defined"      unless defined $type;
    croak "error: no value defined"     unless defined $value;

    # register the given OID and callback
    POE::Kernel->post($self, add_oid_entry => $oid, $type, $value);

    return $self
}


#
# add_oid_tree()
# ------------
sub add_oid_tree {
    my ($self, $new_tree) = @_;

    # check arguments
    croak "error: expected a hashref"   unless ref $new_tree eq "HASH";

    # register the given OID and callback
    POE::Kernel->post($self, add_oid_tree => $new_tree);

    return $self
}


# ==============================================================================
# Hackery
#

{
    package ### live-patch NetSNMP::OID
            NetSNMP::OID;
    sub as_oid { return join ".", "", $_[0]->to_array }
}


__PACKAGE__

__END__

=head1 NAME

POE::Component::NetSNMP::agent - AgentX clients with NetSNMP::agent and POE


=head1 VERSION

Version 0.500


=head1 SYNOPSIS

Like a traditional C<NetSNMP::agent>, made POE aware:

    use NetSNMP::agent;
    use POE qw< Component::NetSNMP::agent >;


    my $agent = POE::Component::NetSNMP::agent->spawn(
        Alias   => "snmp_agent",
        AgentX  => 1,
    );

    $agent->register(".1.3.6.1.4.1.32272", \&agent_handler);

    POE::Kernel->run;
    exit;

    sub agent_handler {
        my ($kernel, $heap, $args) = @_[ KERNEL, HEAP, ARG1 ];
        my ($handler, $reg_info, $request_info, $requests) = @$args;

        # the rest of the code works like a classic NetSNMP::agent callback
        my $mode = $request_info->getMode;

        for (my $request = $requests; $request; $request = $request->next) {
            if ($mode == MODE_GET) {
                # ...
            }
            elsif ($mode == MODE_GETNEXT) {
                # ...
            }
            else {
                # ...
            }
        }
    }

Simpler, let the module do the boring stuff, but keep control of the loop:

    use NetSNMP::ASN;
    use POE qw< Component::NetSNMP::agent >;


    my $agent = POE::Component::NetSNMP::agent->new(
        Alias       => "snmp_agent",
        AgentX      => 1,
        AutoHandle  => ".1.3.6.1.4.1.32272",
    );

    POE::Session->create(
        inline_states => {
            _start => sub {
                $_[KERNEL]->yield("update_tree");
            },
            update_tree => \&update_tree,
        },
    );

    POE::Kernel->run;
    exit;

    sub update_tree {
        my ($kernel, $heap) = @_[ KERNEL, HEAP ];

        # populate the OID tree at regular intervals with
        # the add_oid_entry and add_oid_tree events
    }

Even simpler, let the module do all the boring stuff, leaving you nothing
more to do than providing the handlers to update the OID trees:

    use NetSNMP::ASN;
    use POE::Component::NetSNMP::agent;

    my $agent = POE::Component::NetSNMP::agent->new(
        AgentX      => 1,
        AutoHandle  => ".1.3.6.1.4.1.32272",
        AutoUpdate  => [[ \&update_tree, 30 ]],
    );

    $agent->run;

    sub update_tree {
        my ($self) = @_;

        # populate the OID tree with the add_oid_entry() and add_oid_tree()
        # methods, a la SNMP::Extension::PassPersist
    }

See also in F<eg/> for more ready-to-use examples.


=head1 DESCRIPTION

This module is a thin wrapper around C<NetSNMP::agent> to use it within
a POE-based program, its basic use being the same as you would do
without POE: C<register> one or more OIDs with their associated callbacks,
then within a callback process & answer the requests with C<setValue()>,
C<setOID()>, C<setError()>, etc.

C<POE::Component::NetSNMP::agent> also provides a simpler mechanism,
similar to C<SNMP::Extension::PassPersist>, if you just want to handle
C<get> and C<getnext> requests over an OID tree: set the C<Autohandle>
option to the a OID, then add OID entries with C<add_oid_entry> or
C<add_oid_tree>.

The module will try to automatically recover from a lost connection with
AgentX master (see the C<Ping> option), but you can force a check by
C<post>ing to C<agent_check>;

Note that most of the API is available both as POE events and as object
methods, in an attempt to make it a bit easier for people not fully used
to event programming with POE.

This module can use C<Sort::Key::OID> when it is available, for sorting
OIDs faster than with the internal pure Perl function.


=head1 METHODS

=head2 spawn

Create and return a POE session for handling NetSNMP requests.

B<NetSNMP::agent options>

=over

=item *

C<Name> - I<(optional)> sets the agent name, defaulting to C<"perl">.
The underlying library will try to read a F<$name.conf> Net-SNMP
configuration file.

=item *

C<AgentX> - I<(optional)> be a sub-agent (0 = false, 1 = true).
The Net-SNMP master agent must be running first.

=item *

C<Ports> - I<(optional)> sets the ports this agent will listen
on (e.g.: C<"udp:161,tcp:161">).

=back

B<Component options>

=over

=item *

C<Alias> - I<(optional)> sets the session alias

=item *

C<AutoHandle> - I<(optional)> sets the component to auto-handle C<get>
and C<getnext> request to the given OID

=item *

C<Debug> - I<(optional)> when true, enables debug mode on this session

=item *

C<Ping> - I<(optional)> sets the ping delay between manual agent checks
in seconds; default is 10 seconds

=item *

C<Errback> - I<(optional)> sets the error callback.

=back

B<Example:>

    my $agent = POE::Component::NetSNMP::agent->spawn(
        Alias   => "snmp_agent",
        AgentX  => 1,
    );


=head2 new

Simpler constructor: create all the POE sessions and events to handle
absolutely all the boring stuff; just provide handlers to update the
OID trees. Accept the same arguments than C<span> plus the following:

=over

=item *

C<AutoUpdate> - expects a definition of handlers and their respective
delay (in seconds) in the form of an array of arrays:

    AutoUpdate => [
        [ CODEREF, DELAY ],
        ...
    ],

=back

B<Examples:>

    # create an agent with a single handler, called every 30 sec
    my $agent = POE::Component::NetSNMP::agent->new(
        AgentX      => 1,
        AutoHandle  => ".1.3.6.1.4.1.32272",
        AutoUpdate  => [[ \&update_tree, 30 ]],
    );

    # create an agent with two handlers, called respectively
    # every 10 and every 20 seconds
    my $agent = POE::Component::NetSNMP::agent->new(
        AgentX      => 1,
        AutoHandle  => ".1.3.6.1.4.1.32272",
        AutoUpdate  => [
            [ \&update_tree_1, 10 ],
            [ \&update_tree_2, 20 ],
        ],
    );


=head2 run

Run the main loop (executes C<< POE::Kernel->run >>)


=head2 register

Register a callback handler for a given OID.

B<Arguments:>

=over

=item 1. I<(mandatory)> OID to register

=item 2. I<(mandatory)> request handler callback; must be a coderef

=back

B<Example:>

    $agent->register(".1.3.6.1.4.1.32272.1", \&tree_1_handler);
    $agent->register(".1.3.6.1.4.1.32272.2", \&tree_2_handler);


=head2 add_oid_entry

Add an OID entry to be auto-handled by the agent.

B<Arguments:>

=over

=item 1. I<(mandatory)> OID

=item 2. I<(mandatory)> ASN type; use the constants given by
C<NetSNMP::ASN> like C<ASN_COUNTER>, C<ASN_GAUGE>, C<ASN_OCTET_STR>..

=item 3. I<(mandatory)> value

=back

B<Example:>

    $agent->add_oid_entry(".1.3.6.1.4.1.32272.1", ASN_OCTET_STR, "oh hai");

    $agent->add_oid_entry(".1.3.6.1.4.1.32272.2", ASN_OCTET_STR,
        "i can haz oh-eye-deez??");


=head2 add_oid_tree

Add multiple OID entries to be auto-handled by the agent.

B<Arguments:>

=over

=item 1. I<(mandatory)> OID tree; must be a hashref with the following
structure:

    {
        OID => [ ASN_TYPE, VALUE ],
        ...
    }

=back

B<Example:>

    %oid_tree = (
        ".1.3.6.1.4.1.32272.1" => [ ASN_OCTET_STR, "oh hai" ];
        ".1.3.6.1.4.1.32272.2" => [ ASN_OCTET_STR, "i can haz oh-eye-deez??" ];
    );

    $agent->add_oid_tree(\%oid_tree);


=head2 heap

Access the heap dedicated to update handlers.


=head1 POE EVENTS

=head2 register

Register a callback handler for a given OID.

B<Arguments:>

=over

=item ARG0: I<(mandatory)> OID to register

=item ARG1: I<(mandatory)> request handler callback; must be an event
name or a coderef

=back

B<Example:>

    POE::Kernel->post($agent, register => ".1.3.6.1.4.1.32272.1", "tree_1_handler");
    POE::Kernel->post($agent, register => ".1.3.6.1.4.1.32272.2", "tree_2_handler");


=head2 add_oid_entry

Add an OID entry to be auto-handled by the agent.

B<Arguments:>

=over

=item ARG0: I<(mandatory)> OID

=item ARG1: I<(mandatory)> ASN type; use the constants given by
C<NetSNMP::ASN> like C<ASN_COUNTER>, C<ASN_GAUGE>, C<ASN_OCTET_STR>..

=item ARG2: I<(mandatory)> value

=back

B<Example:>

    POE::Kernel->post($agent, add_oid_entry =>
        ".1.3.6.1.4.1.32272.1", ASN_OCTET_STR, "oh hai");

    POE::Kernel->post($agent, add_oid_entry =>
        ".1.3.6.1.4.1.32272.2", ASN_OCTET_STR, "i can haz oh-eye-deez??");


=head2 add_oid_tree

Add multiple OID entries to be auto-handled by the agent.

B<Arguments:>

=over

=item ARG0: I<(mandatory)> OID tree; must be a hashref with the following
structure:

    {
        OID => [ ASN_TYPE, VALUE ],
        ...
    }

=back

B<Example:>

    %oid_tree = (
        ".1.3.6.1.4.1.32272.1" => [ ASN_OCTET_STR, "oh hai" ];
        ".1.3.6.1.4.1.32272.2" => [ ASN_OCTET_STR, "i can haz oh-eye-deez??" ];
    );

    POE::Kernel->post($agent, add_oid_tree => \%oid_tree);



=head1 SEE ALSO

L<POE>, L<http://poe.perl.org/>

L<NetSNMP::agent>, L<NetSNMP::ASN>, L<NetSNMP::OID>

Net-SNMP web site: L<http://www.net-snmp.org/>


=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc POE::Component::NetSNMP::agent

You can also look for information at:

=over

=item * Search CPAN

L<http://search.cpan.org/dist/POE-Component-NetSNMP-agent/>

=item * Meta CPAN

L<https://metacpan.org/release/POE-Component-NetSNMP-agent>

=item * RT: CPAN's request tracker (report bugs here)

L<https://rt.cpan.org/Public/Dist/Display.html?Name=POE-Component-NetSNMP-agent>

=item * AnnoCPAN: Annotated CPAN documentation

L<http://annocpan.org/dist/POE-Component-NetSNMP-agent>

=item * CPAN Ratings

L<http://cpanratings.perl.org/d/POE-Component-NetSNMP-agent>

=back


=head1 CAVEATS

A known issue with this module is that if the snmpd daemon it is connected
to dies, the default POE loop will spin over the half-closed Unix socket,
eating 100% of CPU until the daemon is restarted and the sub-agent has
reconnected. However, this problem can be worked around by selecting
an alternative loop like AnyEvent, EV or EPoll (the other loops have the
same bug).

L<https://github.com/maddingue/POE-Component-NetSNMP-agent/issues/1>


=head1 BUGS

Please report any bugs or feature requests to
C<bug-poe-component-netsnmp-agent at rt.cpan.org>,
or through the web interface at
L<https://rt.cpan.org/Public/Dist/Display.html?Name=POE-Component-NetSNMP-agent>.
I will be notified, and then you'll automatically be notified of
progress on your bug as I make changes.


=head1 ACKNOWLEDGEMENTS

Thanks to Rocco Caputo and Rob Bloodgood for their help on C<#poe>.


=head1 AUTHOR

SE<eacute>bastien Aperghis-Tramoni C<< <sebastien at aperghis.net> >>


=head1 LICENSE AND COPYRIGHT

Copyright 2011 SE<eacute>bastien Aperghis-Tramoni.

This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.

See L<http://dev.perl.org/licenses/> for more information.

=cut


=begin comment

The interaction problem between NetSNMP::agent and Perl
- - - - - - - - - - - - - - - - - - - - - - - - - - - -

The root of the problem is in the way Perl manages the lexical variables,
which is exposed by this short example:

    use strict;
    use Devel::Peek;

    my @a = ( "hai", 42 );

    for (0..$#a) {
        my $v = $a[$_];
        Dump $v;
    }

which, when executed, produces:

    SV = PV(0x504270) at 0x51f0b0
      REFCNT = 1
      FLAGS = (PADBUSY,PADMY,POK,pPOK)
      PV = 0x511a50 "hai"\0
      CUR = 3
      LEN = 4
    SV = PVIV(0x5050d0) at 0x51f0b0
      REFCNT = 1
      FLAGS = (PADBUSY,PADMY,IOK,pIOK)
      IV = 42
      PV = 0x511a50 "hai"\0
      CUR = 3
      LEN = 4

That is, the fields of the scalar are not erased, and "pollutes" the
scalar in the next loop, and what was a pure integer (IV) suddenly
becomes a PVIV. In Perl land, this is not a problem because perl DTRT.
However, in XS land, one must be more cautious, and the implementation
of setValue() in NetSNMP::agent (from version 5.0 up to 5.7) is somehow
incorrect and as a matter of fact can't handle PVIV scalars, throwing
the delightful "Non-unsigned-integer value passed to setValue" error.

See also this discussion on Perl 5 Porters:
http://www.nntp.perl.org/group/perl.perl5.porters/2011/08/msg175527.html

A first workaround is to stringify everything. but that's kind of icky.

A second one is to add a level of indirection:

    for my $i (0..$#values) {
        my $v = \$values[$i];
        Dump $$v;
    }

A third one is simply to not use any intermediate lexical variables.
This solution is the one now used in this module.

=end comment