#!/usr/bin/perl

use strict;
use Error;
use RRD::Query;
use RRD::Threshold;

# $Id: rrdpoller,v 1.7 2004/12/06 18:21:11 rs Exp $
$main::VERSION = "1.0.2";

=pod

=head1 NAME

rrdpoller - Retrieve RRD file data and apply some threshold algorithm

=head1 SYNOPSIS

B<rrdpoller> B<list|ls> I<filename>

B<rrdpoller> B<get> I<filename> I<datasource> [B<--offset> I<time>]
[B<--cf> I<func-name>]

B<rrdpoller> B<exact> I<filename> I<datasource> I<value>

B<rrdpoller> B<boundaries> I<filename> I<datasource> [B<--min> I<number>]
[B<--max> I<number>]

B<rrdpoller> B<relation> I<filename> I<datasource> [I<E<lt>>|I<E<gt>>]I<threshold>[I<%>]
[B<--target> I<filename>] [B<--compare-ds> I<datasource>] [B<--offset> I<time>]

B<rrdpoller> B<quotient> I<filename> I<datasource> [I<E<lt>>|I<E<gt>>]I<threshold>[I<%>]
[B<--target> I<filename>] [B<--target-ds> I<datasource>] [B<--offset> I<time>]

B<rrdpoller> B<hunt> I<filename> I<datasource> I<roll> [B<--parent> I<filename>]
[B<--parent-ds> I<datasource>]

=head1 DESCRIPTION

B<rrdpoller> allows you to poll RRD files and get current value of a
given datasource. Additionally it implement some advanced checks able
to use past values to decide if the current one is out of threshold or
not.

The main purpose of this tool is to do the interface between an
existing monitoring system like BigBrother, Nagios or Mon and a set of
RRD graphs generated by some specialized data collectors like
rrdcollect, Cricket and such. Thus you can build a very modular
monitoring/trend architecture without the need of double polling.

=head1 EXAMPLES

Those examples are fictive, they doesn't represent real numbers. It's
just to let you figure out what you can do with each algorithm.

=over 4

=item B<rrdpoller> B<ls> host1-net.rrd

Returns the whole list of datasources contained in the file.

=item B<rrdpoller> B<get> host1-load.rrd 1min_avg

Get the current value of the I<1min_avg> datasource of the
I<host1-load.rrd> file.

=item B<rrdpoller> B<exact> host1-service.rrd http 1

Get the current value of the I<http> datasource of the
I<host1-load.rrd> file and return false if not equal to 1.

=item B<rrdpoller> B<boundaries> host1-load.rrd 5min_avg B<--max> 15

Get the current value of the I<5min_avg> datasource of the
I<host1-load.rrd> file and return false if greater than I<15>.

=item B<rrdpoller> B<relation> host1-disk.rrd usage 100000 B<--offset> 1min

Compare the current I<usage> datasource value of the I<host1-disk.rrd>
file with the same datasource 1 minute ago and return false if the
delta is greater than 100000.

=item B<rrdpoller> B<quotient> host1-mem.rrd usage 90% B<--target-ds> total

Compare the current I<usage> datasource of the I<host1-mem.rrd> file
and divide it by the I<total> datasource of the same file. The command
returns false if the obtained percentage is greater than I<90%>.

=item B<rrdpoller> B<hunt> pop2.rrd users 40 B<--target> pop1.rrd

If the I<users> datasource of the I<pop2.rrd> file is non-zero and the
same datasource of the I<pop1.rrd> hasn't yet reach I<40>, the command
will return false.

=back

=cut

my %args;
my @argv;
my $_arg;
while(@ARGV)
{
    $_arg = shift;
    if(index($_arg, '--') == 0)
    {
        $args{substr($_arg, 2)} = shift || usage();
    }
    else
    {
        push(@argv, $_arg);
    }
}

my $command = shift(@argv) || usage();
if($command !~ /^(list|ls|get|exact|boundaries|relation|quotient|hunt)$/)
{
    usage();
}

my $filename   = shift(@argv) || usage();
my $datasource;
if($command ne 'list' and $command ne 'ls')
{
    $datasource = shift(@argv) || usage();
}

{
    no strict qw(refs);
    &{$command};
}

=pod

=head1 SUB-COMMANDS

All sub-commands needs at least the too first arguments I<filename>
and I<datasource> (except for list command). Except for the B<get> and
B<list> commands (which doesn't perform a test), all sub-command will
change the rrdpoller return code to false (1) if the test fails. All
sub-commands will print the given I<datasource> value of the given
I<filename> on the standard output.

=head2 B<list|ls> I<filename>

This command list all datasources of the given file.

=cut

sub ls {return list(@_)}
sub list
{
    (@argv || %args) && usage();

    my $rrd = new RRD::Query($filename);
    my $list = $rrd->list();
    print(join("\n", @$list), "\n");
}

=pod

=head2 B<get> I<filename> I<datasource> B<[OPTIONS]>

This sub-command the last inserted value in the given I<datasource> of
the given RRD I<filename>. You can get an older value by giving an
offset that will be expressed as: <last_inserted_time> - <offset>.

=head3 options:

=over 4

=item B<--offset> I<time>

Makes the B<get> command to retrieve an earlier value. It substract
the value of I<time> to the timestamp of the last value inserted in
the I<datasource>. The I<time> can be expressed in second (ie I<60>)
or in RRD time reference specification format (see L<rrdfetch/"TIME
REFERENCE SPECIFICATION">).

=item B<--cf> I<func-name>

Default consolidation function is AVERAGE. This option allow you to
change it to whichever you want.

=back

=cut

sub get
{
    my $offset = delete($args{offset}) || 0;
    my $cf = delete($args{cf});
    (@argv || %args) && usage();

    my $rrd = new RRD::Query($filename);
    my $value = $rrd->fetch($datasource, offset => $offset, cf => $cf);
    print "$value\n";
}

=pod

=head2 B<exact> I<filename> I<datasource> I<exact>

This threshold allows you to monitor the I<datasource> for an I<exact>
match. This is useful in cases where an enumerated (or boolean) SNMP
object instruments a condition where a transition to a specific state
requires attention. For example, a datasource might return either
true(1) or false(2), depending on whether or not a power supply has
failed.

=cut

sub exact
{
    my $exact = shift(@argv);
    (@argv || %args) && usage();

    my $rt = new RRD::Threshold();
    my($value, $bool) = $rt->exact($filename, $datasource, $exact);
    print "$value\n";
    exit($bool ? 0 : 1);
}

=pod

=head2 B<boundaries> I<filename> I<datasource> B<[OPTIONS]>

This threshold takes too optional values, a I<min>imum and a
I<max>imum value. If the I<datasource> strays outside of this
interval, the test fail.

=head3 options:

=over 4

=item B<--min> I<number>

If current value is lower than I<number>, the test fail

=item B<--max> I<number>

If current value is greater than I<number>, the test fail

=back

=cut

sub boundaries
{
    my $min = delete($args{min});
    my $max = delete($args{max});
    (@argv || %args) && usage();

    my %opt;
    $opt{min} = $min if defined $min;
    $opt{max} = $max if defined $max;
    my $rt = new RRD::Threshold();
    my($value, $bool) = $rt->boundaries($filename, $datasource, %opt);
    print "$value\n";
    exit($bool ? 0 : 1);
}

=pod

=head2 B<relation> I<filename> I<datasource> I<[E<lt>|E<gt>]threshold[%]> B<[OPTIONS]>

A relation threshold considers the difference between two
I<datasources> (possibly from different RRD I<files>), or
alternatively, the difference between two temporally distinct values
for the same I<datasource>. The difference can be expressed as
absolute value, or as a percentage of the second I<datasource>
(comparison) value. This difference is compared to a I<threshold>
argument with either the greater than (>) or lesser than (<)
operator. The criteria fails when the expression (<absolute or
relative difference> <either greater-than or less-than> <threshold>)
evaluates to false.

The I<threshold> number, optionally preceded by the greater than (>)
or less than (<) symbol, and optionally followed by the symbol percent
(%). If omitted, greater than is used by default and the expression:
difference > threshold, is evaluated. "<10%", ">1000", "50%", and
"500" are all examples of valid thresholds.

=head3 options:

=over 4

=item B<--target> I<filename>

The path of the comparison RRD I<file>. This argument is optional and
if omitted the first RRD I<file> is also taken as the comparison target.

=item B<--target-ds> I<datasource>

The name of the comparison I<datasource>. This I<datasource> must
belong to the comparison RRD file. This argument is optional and if
omitted the first I<datasource> name is also taken as the comparison
datasource name. If the value is a number, the value is considered as
a fix value and is taked for the comparison.

=item B<--offset> I<time>

The temporal I<offset> to go back from the first value to fetch the
comparison datasource value. Note that a data source value must exist
in the RRD file for that exact offset. If This argument is optional
and if omitted, it is set to 0. The I<time> value can be expressed in
second (ie I<60>) or in RRD time reference specification format (see
L<rrdfetch/"TIME REFERENCE SPECIFICATION">).

=back

=cut

sub relation
{
    my $threshold = shift(@argv) || usage();
    my $target_file = delete($args{target});
    my $target_ds = delete($args{'target-ds'});
    my $offset = delete($args{offset});
    (@argv || %args) && usage();

    my %opt;
    $opt{cmp_rrdfile} = $target_file if defined $target_file;
    $opt{cmp_ds} = $target_ds if defined $target_ds;
    $opt{offset} = $offset if defined $offset;
    my $rt = new RRD::Threshold();
    my($value, $bool) = $rt->relation($filename, $datasource, $threshold, %opt);
    print "$value\n";
    exit($bool ? 0 : 1);
}

=pod

=head2 B<quotient> I<filename> I<datasource> I<[E<lt>|E<gt>]threshold[%]> B<[OPTIONS]>

Quotient thresholds are similar to relation thresholds, except that
they consider the quotient of two data sources, or alternatively, the
same data source at two different time points. For a quotient monitor
threshold, the value of the first data source is computed as a
percentage of the second data source value (such as 10 (first
datasource) is 50% of 20 (second datasource)). This percentage is then
compared to a threshold argument with either the greater than (>) or
less than (<) operator. The criteria fails when the expression
(<percentage> <either greater-than or less-than> <threshold>)
evaluates to false.

The I<threshold> number, optionally preceded by the greater than (>)
or less than (<) symbol and followed by the symbol percent (%). If
omitted, greater than is used by default and the expression:
difference > threshold, is evaluated. "<10%" and "50%" are all
examples of valid thresholds.

=head3 options:

=over 4

=item B<--target> I<filename>

The path of the comparison RRD I<file>. This argument is optional and
if omitted the first RRD I<file> is also taken as the comparison target.

=item B<--target-ds> I<datasource>

The name of the comparison I<datasource>. This I<datasource> must
belong to the comparison RRD file. This argument is optional and if
omitted the first I<datasource> name is also taken as the comparison
datasource name. If the value is a number, the value is considered as
a fix value and is taked for the comparison.

=item B<--offset> I<time>

The temporal I<offset> to go back from the first value to fetch the
comparison datasource value. Note that a data source value must exist
in the RRD file for that exact offset. If This argument is optional
and if omitted, it is set to 0. The I<time> value can be expressed in
second (ie I<60>) or in RRD time reference specification format (see
L<rrdfetch/"TIME REFERENCE SPECIFICATION">).

=back

=cut

sub quotient
{
    my $threshold = shift(@argv) || usage();
    my $target_file = delete($args{target});
    my $target_ds = delete($args{'target-ds'});
    my $offset = delete($args{offset});
    (@argv || %args) && usage();

    my %opt;
    $opt{cmp_rrdfile} = $target_file if defined $target_file;
    $opt{cmp_ds} = $target_ds if defined $target_ds;
    $opt{offset} = $offset if defined $offset;
    my $rt = new RRD::Threshold();
    my($value, $bool) = $rt->quotient($filename, $datasource, $threshold, %opt);
    print "$value\n";
    exit($bool ? 0 : 1);
}

=pod

=head2 B<hunt> I<roll> B<--parent> <filename> B<--parent-ds> <datasource> B<[OPTIONS]>

The hunt threshold is designed for the situation where the data source
serves as an overflow for another data source; that is, if one data
source (the parent) is at or near capacity, then traffic will begin to
appear on this (the monitored) data source.One application of hunt
monitor thresholds is to identify premature rollover in a set of modem
banks configured to hunt from one to the next. Specifically, the
criteria of the hunt monitor threshold fails if the value of the
monitored data source is non-zero and the current value of the parent
data source falls below a specified capacity threshold.

The I<roll> argument is the threshold of the parent data
source. Generally this should be slightly less than the maximum
capacity of the target.

=head3 options:

=over 4

=item B<--parent> I<filename>

The path of the comparison RRD I<file>. This argument is optional and
if omitted the first RRD I<file> is also taken as the comparison target.

=item B<--parent-ds> I<datasource>

The name of the comparison I<datasource>. This I<datasource> must
belong to the comparison RRD file. This argument is optional and if
omitted the first I<datasource> name is also taken as the comparison
datasource name. If the value is a number, the value is considered as
a fix value and is taked for the comparison.

=back

=cut

sub hunt
{
    my $roll = shift(@argv) || usage();
    my $parent_file = delete($args{parent});
    my $parent_ds = delete($args{'parent-ds'});
    (@argv || %args) && usage();

    my %opt;
    $opt{cmp_rrdfile} = $parent_file if defined $parent_file;
    $opt{cmp_ds} = $parent_ds if defined $parent_ds;
    my $rt = new RRD::Threshold();
    my($value, $bool) = $rt->hunt($filename, $datasource, %opt);
    print "$value\n";
    exit($bool ? 0 : 1);
}

sub usage
{
    print STDERR <<EOT;
Syntax:

rrdpoller list|ls <filename>

rrdpoller get <filename> <datasource> --offset <time> --cf <func-name>

rrdpoller exact <filename> <datasource> <value>

rrdpoller boundaries <filename> <datasource> [--min <number>] [--max <number>]

rrdpoller relation <filename> <datasource> <[<|>]threshold[%]> [--target <filename>]
  [--compare-ds <datasource>] [--offset <time>]

rrdpoller quotient <filename> <datasource> <[<|>]threshold[%]> [--target <filename>]
  [--target-ds <datasource>] [--offset <time>]

rrdpoller hunt <filename> <datasource> <roll> [--target <filename>]
  [--target-ds <datasource>]
EOT
    exit;
}

=pod

=head1 EXIT STATUS

=over 4

=item B<0>

Successful test

=item B<1>

Performed test return a false value

=back

=head1 AUTHOR

Olivier Poitrey E<lt>rs@rhapsodyk.netE<gt>

=head1 LICENCE

rrdpoller retrieves RRD file data and apply some threshold algorithm.
Copyright (C) 2004  Olivier Poitrey

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or (at
your option) any later version.

This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA

=head1 SEE ALSO

L<RRD::Query>, L<RRD::Threshold>, L<rrdtool>, L<RRDs>

=cut
