#!/usr/bin/perl

use strict;

use XML::DOM;
use IPC::SysV qw( IPC_CREAT IPC_EXCL S_IRWXU IPC_NOWAIT);
use IPC::Msg;
use sigtrap qw (die INT TERM QUIT HUP); 
use Socket;
use LogTrend::Agent::SNMPAgent::Traps;
use Getopt::Long;
use POSIX qw( setsid );
use LogTrend::Common::LogDie;

my($progname) = 'SNMPTrapd';

my($qid)=1024;
my($port) = 162;
my($timeout) = 5;
my($daemon) = 1;
my($closing) = 1;


my($msgqueue);
my($traps);
my($regtable) = {};
my($alarmtable) = {};
my($worktable) = {};


END {
        $closing
    or  return;

        $msgqueue
    and do {
        SysLog "Removing message queue [$qid]...\n";
        $msgqueue->remove;
    };

        $traps
    and do {
        SysLog "Closing trap listener...\n";
        $traps->close();
    };

    SysLog "ended."
}

sub Usage {
    die "Usage: $0 [--nodaemon] [--port|-p number] [--timeout|-t number] [--queueid|-q number]\n",
        "\n",
        "        --nodaemon: specify not to become a daemon (default daemon)\n",
        "        --port: specify the listening port number for traps. (default 162)\n",
        "        --timeout: specify the timeout for processing traps (default 5)\n",
        "        --queueid: specify the IPC Message queue ID (default 1024)\n";
    exit(1);
}

sub Options {
    Getopt::Long::Configure("bundling", "no_ignore_case");
        GetOptions( 'daemon!' => \$daemon,
                    'port|p=i' => \$port ,
                    'timeout|t=i' => \$timeout ,
                    'queueid|q=i' => \$qid)
    or  Usage();
}

#
# INPUT: PID, [ NUM_ALARM, IP, [ OID, value|undef, ...],
#               NUM_ALARM, IP, [ OID, value|undef, ...]... ]
#
sub RegisterPID {
    my($pid, $alarms) = @_;

        exists($regtable->{$pid})
    and die "RegisterPID [$pid]: process already registered";

    while(@$alarms) {
        my($num) = shift(@$alarms);
        my($ip) = shift(@$alarms);
        my($oids) = shift(@$alarms);
        my($nip);

            $nip = inet_aton($ip)
        or  die "Invalid host name or ip address: '$ip'";

        $alarmtable->{$pid} = {};
        ++$regtable->{$pid}{$nip};
        push(@{$worktable->{$nip}{$pid}}, { num => $num, oids => $oids });
    }
}

sub UnregisterPID {
    my($pid) = @_;

        exists($regtable->{$pid})
    or  die "UnregisterPID [$pid]: process not registered";

    delete($alarmtable->{$pid});
    for my $ip (keys(%{$regtable->{$pid}})) {
        delete($worktable->{$ip}{$pid});
    }
    delete($regtable->{$pid});
}

sub LogTrapBag {
    my($ip, $bag) = @_;

        exists($worktable->{$ip})
    or  return;

    for my $pid (keys(%{$worktable->{$ip}})) {
        my($alarmlist) = $worktable->{$ip}{$pid};

        ALARM:
        for my $alarm (@$alarmlist) {
            my($num) = $alarm->{num};
            my($oids) = $alarm->{oids};

            for(my $i = 0; $i < @$oids; ) {
                my($oid) = $oids->[$i++];
                my($value) = $oids->[$i++];

                    exists($bag->{$oid})
                or  next ALARM;

                    defined($value)
                and $value ne $bag->{$oid}
                and next ALARM;

            }
            ++$alarmtable->{$pid}{$num};
        }
    }
}

sub GetAlarms {
    my($pid) = @_;
    my($alarms);

        exists($regtable->{$pid})
    or  die "GetAlarms [$pid]: process not registered";

    $alarms = [ keys(%{$alarmtable->{$pid}}) ];
    $alarmtable->{$pid} = {};
    $alarms
}

sub ProcessMessage {
    my($mess) = @_;
    my($pid);

    my($Answer) = sub {
        my($status, $data) = @_;

            $pid
        or  SysLog "[no pid]: $data\n$mess";

            kill(0, $pid)
        or  SysLog "[$pid] not running: $data\n$mess";

            $msgqueue->snd($pid, "<Answer status=\"$status\">$data</Answer>")
        or  die "Failed to deliver message to [$pid]: $!\n";
    };

    my($Process) = sub {
        my($parser) = new XML::DOM::Parser;
        my($node) = $parser->parse($mess);
        my($type);

        my($RegisterMessage) = sub {
            my($node) = @_;
            my($traps) = [ ];

            for my $node ($node->getElementsByTagName('Trap', 0)) {
                my($num, $ip, $objects);

                    $num = $node->getAttribute('num')
                or  die "Tag 'Trap': missing attribute 'num'";

                    $ip = $node->getAttribute('ip')
                or  die "Tag 'Trap': missing attribute 'ip'";

                for my $node ($node->getElementsByTagName('Object', 0)) {
                    my($oid, $value);

                        $oid = $node->getAttribute('oid')
                    or  die "Tag 'Object': missing attribute 'oid'";

                        $value = $node->getAttribute('value')
                    or  $value = undef;

                    push(@$objects, $oid, $value );
                }

                    @$objects
                or  die "Tag 'Trap': missing tag(s) 'Object'";

                push(@$traps, $num, $ip, $objects);
            }

                @$traps
            or  die "Tag 'Pid': missing tag(s) 'Trap' ";

            RegisterPID($pid, $traps);
            &$Answer('ok', '');
        };

        my($GetMessage) = sub {
            my($node) = @_;
            my($collection) = GetAlarms($pid);
            
            &$Answer('ok', join(',', @$collection));
        };


        my($UnregisterMessage) = sub {
            my($node) = @_;

            UnregisterPID($pid);
            &$Answer('ok');
        };

            $node = $node->getElementsByTagName('Message', 0)->item(0)
        or  die "Tag 'Message': missing tag(s) 'Trap' ";

            $pid = $node->getAttribute('pid')
        or  die "Tag 'Message': missing attribute 'pid'";

            $type = $node->getAttribute('type')
        or  die "Tag 'Message': missing attribute 'type'";

        for($type) {
            /^R$/ and do {
                    &$RegisterMessage($node);
                    last;
                };
            /^G$/ and do {
                    &$GetMessage($node);
                    last;
                };
            /^U$/ and do {
                    &$UnregisterMessage($node);
                    last;
                };
            die "Unknown message type '$_'";
        }
    };

    eval { &$Process(@_) };

        $@
    and do {
        my($error) = $@;

        chomp($error);
        eval { &$Answer('nok', "$error") };
            $@
        and SysLog "$@";
    };

}

sub Daemonize {
    my($pid);

        chdir '/'
    or  die "Can't chdir to /: $!";
        open STDIN, '/dev/null'
    or  die "Can't open /dev/null for reading: $!";
        open STDOUT, '>/dev/null'
    or  die "Can't open /dev/null for writing: $!";

    $pid = fork();

        $pid < 0
    and die "Can't fork: $!";

        $pid
    and do {
        $closing = 0;
        exit(0);
    };

        setsid()
    or  die "Can't start a new session: $!";

        open STDERR, '>&STDOUT'
    or  die "Can't dup strerr to stdout: $!";
}


Options();

eval { $msgqueue = new IPC::Msg($qid, IPC_CREAT | IPC_EXCL | S_IRWXU ) };

    $@
and die "Cannot create message queue:\n$@";

eval { $traps = new LogTrend::Agent::SNMPAgent::Traps( Port => $port,
                                                       Timeout => $timeout) };

    $@
and die "Cannot create trap listener:\n$@";

if($daemon) {
    OpenLog($progname,'daemon');
    Daemonize();
}
else {
    OpenLog($progname,'nodaemon');
}

SysLog "started.";

while(1) {
    my($trapbag) = $traps->GetTraps();

        $trapbag
    and LogTrapBag(@$trapbag);

    while(1) {
        my($msg);

        $msgqueue->rcv($msg, 4096, 1, IPC_NOWAIT);

            $msg
        or  last;

        ProcessMessage($msg);
    }
}

1;

__END__


=head1 NAME

SNMPTrapd - Perl script for LogTrend : SNMP trap collortor for SNMP Agents

=head1 SYNOPSIS

    SNMPTrapd [--nodaemon] [--port|-p number] [--timeout|-t number] [--queueid|-q number]

          --nodaemon: specify not to become a daemon (default daemon)

          --port: specify the listening port number for traps. (default 162)

          --timeout: specify the timeout for processing traps (default 5)

          --queueid: specify the IPC Message queue ID (default 1024)

=head1 DESCRIPTION

SNMPTrapd is a Perl daemon for use in conjonction with the LogTrend SNMP Agent.

Its sole purpose is to listen for SNMP traps and IPC messages requests from the
agents and return them the trap bags they requested to be alerted with.

Its main loop consists of waiting on port C<port> C<timeout> time for traps.

Then, if one trapbag has been caught, it looks if it matches a request from an
agent. If not, the trap bag is discarded, otherwise, the information will be
sent to the agent when it will request it.

Finally, the IPC message queue (number C<queueid>) is processed by reading
on messages of type 1 and writing with a message type corresponding to the
agent process number.

The exchanged messages are in valid XML. Requests always originate from clients
(agents) and always get an answer from the server (SNMPTrapd).

They can be of three types:

=over 4

=item Register

With this request, the client passes the server a set of informations about
its pid and the matching traps it wants to be alerted. The message is of the
form:

    <Message pid="pid" type="R">
        <Trap num="alarm number" ip="source IP address">
            <Object oid="OBJECT-IDENTIFIER" value="optionnal value" />
            ...
        </Trap>
        ...
    </Message>

=item Unregister
As its name suggest it, this request is sent by the client to tell the
server it can release the resources handled by the agent. Its form is:

    <Message pid="pid" type="U" />

=item Get
The client request the list of alarms it has registered for:

    <Message pid="pid" type="G" />


=back

For each request, the server answers back a message of the form:

    <Answer status="status">
        data
    </Answer>

where status is one of C<ok> or C<nok> and data can be empty. If the status
is C<nok>, then data contains a description of the encountered problem, and
if the request was of type GET and alarms are waiting for the agent, it
contains a comma separated list of alarm numbers.

When alarms have been collected by the agent, they are discarded from the
server tables.

=head1 AUTHOR

Franois Dsarmnien -- Atrid Systmes (f.desarmenien@atrid.fr)

=head1 COPYRIGHT

Copyright 2001, Atrid Systme.

Licensed under the same terms as LogTrend project is.

=head1 WARRANTY

THIS SOFTWARE COMES WITH ABSOLUTLY NO WARRANTY OF ANY KIND.
IT IS PROVIDED "AS IS" FOR THE SOLE PURPOSE OF EVENTUALLY
BEEING USEFUL FOR SOME PEOPLE, BUT ONLY AT THEIR OWN RISK.


=cut



