#!/usr/bin/perl -w

use 5.006;
use strict;
use warnings;

# braucht:
# * libfile-readbackwards-perl >= 1.03
# * libnetsaint-noris-perl
# * perl-base

use FindBin ();
use lib $FindBin::Bin;
use File::ReadBackwards ();
use Getopt::Long qw(GetOptions);
use noris::NetSaint;

use constant DEFAULT_STATS_COMMAND => qw(/usr/sbin/rndc stats);

GetOptions
  'critical=s'      => \our %Critical,
  'decimals=i'      => \( our $Decimals = 0 ),
  'debug+'          => \our $Debug,
  'max-wait=i'      => \( our $MaxWait = 10 ),
  'stats-command=s' => \our @StatsCommand,
  'stats-file=s'    => \( our $StatsFile = '/var/log/named/named.stats' ),
  'warnings=s'      => \our %Warning,
  'help|?'          => \our $Help;

@StatsCommand = DEFAULT_STATS_COMMAND unless @StatsCommand;

exec perldoc => -F => $0 or die "exec('perldoc -F $0'): $!\n" if $Help;

our $NetSaint = new noris::NetSaint;

my $bw = File::ReadBackwards->new($StatsFile)
  or $NetSaint->update(
    Unknown => qq(Kann Stats-Datei "$StatsFile" nicht ffnen: $!\n") ), exit;
my $pos = $bw->tell;
system @StatsCommand
  and
  $NetSaint->update( Unknown => qq("@StatsCommand" schlug fehl mit Status $?)
      . ( $! ? ": $!" : '.' ) ), exit;

my $old_ts;
while ( defined( my $line = $bw->readline ) ) {
    ($old_ts) = $line =~ /^--- Statistics Dump --- \((\d+)\)$/ and last;
}
$NetSaint->update( Unknown =>
      qq(Habe keinen alten Timestamp gefunden.)
  ), exit
  unless defined $old_ts;

sub parse_stats {
    my ( %stat, @path );
    for (@_) {
        if ( !@path && /^(\w+) (\d+)$/ ) { $stat{$_} += $2 for $1, '' }
        elsif (/^\+\+ (.*) \+\+$/) { @path = $1 }
        elsif (/^\[(.*)\]$/) {
            $NetSaint->update(
                Unknown => qq(View "$1" auerhalb eines Abschnitts: $_) ), exit
              unless @path;
            $path[1] = $1;
        }
        elsif (/^\s*(\d+) (.*)/) {
            for ( [$2], [] ) {
                ( my $key = join '/', @path, @$_ ) =~ y/ /_/;
                $stat{$key} += $1;
            }
        }
        else {
            $NetSaint->update( Unknown => qq(Unbekannter Eintrag: $_) );
            exit;
        }
    }
    %stat;
}

my %old_v;
{
    my @old_stats;
    while ( defined( my $line = $bw->readline ) ) {
        last if $line =~ /^\+\+\+ Statistics Dump \+\+\+ \($old_ts\)$/;
        unshift @old_stats, $line;
    }
    %old_v = parse_stats(@old_stats);
}

my $fh = $bw->get_handle;

my $new_ts;
for ( 1 .. $MaxWait ) {
    seek $fh, $pos, 0
      or $NetSaint->update( Unknown =>
qq(Kann nicht an Position $pos der Stats-Datei "$StatsFile" springen: $!)
      ), exit;
    if ( defined( my $line = <$fh> ) ) {
        ($new_ts) = $line =~ /^\+\+\+ Statistics Dump \+\+\+ \((\d+)\)$/
          and last;
        $NetSaint->update( Unknown =>
qq(Anstelle eines neuen Timestamps fand ich folgende Zeile vor: $line)
        );
        exit;
    }
    sleep 1;
}

$NetSaint->update( Unknown =>
      qq(Habe keinen neuen Timestamp gefunden.)
  ), exit
  unless defined $new_ts;
( my $seconds = $new_ts - $old_ts ) > 0
  or $NetSaint->update( Unknown =>
"Der neue Timestamp $new_ts ist nicht grer als der alte Timestamp $old_ts."
  ), exit;
print 'Untersuche Zeitraum '
  . localtime($old_ts) . ' bis '
  . localtime($new_ts)
  . " ($seconds Sekunde"
  . ( $seconds != 1 && 'n' ) . "):\n"
  if $Debug;

my %new_v;
{
    my @new_stats;
    while ( defined( my $line = <$fh> ) ) {
        last if $line =~ /^--- Statistics Dump --- \($new_ts\)$/;
        push @new_stats, $line;
    }
    %new_v = parse_stats(@new_stats);
}

my $f = $Decimals ? 5 + $Decimals : 4;
my %relevant = $Debug ? ( %old_v, %new_v ) : ( %Warning, %Critical );
for ( sort keys %relevant ) {
    if ( defined $old_v{$_} ) {
        if ( defined $new_v{$_} ) {
            my $rate = ( $new_v{$_} - $old_v{$_} ) / $seconds;
            printf "%10d /%10d =>%$f.${Decimals}f %s\n", $old_v{$_}, $new_v{$_},
              $rate, $_
              if $Debug;
            if ( defined $Critical{$_} && $rate >= $Critical{$_} ) {
                $NetSaint->update(
                    Critical => sprintf
qq(Die Frequenz fr Schlssel "%s" betrgt %.${Decimals}f pro Sekunde und berschreitet damit den kritischen Schwellenwert %.${Decimals}f.),
                    $_, $rate, $Critical{$_}
                );
            }
            elsif ( defined $Warning{$_} && $rate >= $Warning{$_} ) {
                $NetSaint->update(
                    Warning => sprintf
qq(Die Frequenz fr Schlssel "%s" betrgt %.${Decimals}f pro Sekunde und berschreitet damit den Warnungs-Schwellenwert %.${Decimals}f.),
                    $_, $rate, $Warning{$_}
                );
            }
        }
        else {
            $NetSaint->update( Unknown =>
                  qq(Fr Schlssel "$_" wurde kein neuer Wert gefunden.) );
        }
    }
    elsif ( defined $new_v{$_} ) {
        $NetSaint->update(
            Unknown => qq(Fr Schlssel "$_" wurde kein alter Wert gefunden.) );
    }
    else {
        $NetSaint->update(
            Unknown => qq(Fr Schlssel "$_" wurden keine Werte gefunden.) );
    }
}

$NetSaint->atleast('OK');

__END__

=head1 NAME

check_named-stats -
Nagios-Plugin, das BINDs Befindlichkeit anhand des statistics-file berprft

=head1 SYNOPSE

    check_named-stats -debug

... zeigt eine bersicht der mglichen Messgren sowie der aktuellen Werte
an.

Fr BIND < 9.5:

    check_named-stats -warning =1000 \
                      -critical failure=100 \
                      -warning failure=50

Fr BIND 9.5:

    check_named-stats \
        -warning  Name_Server_Statistics/IPv4_requests_received=1000 \
        -critical Name_Server_Statistics/queries_resulted_in_SERVFAIL=100 \
        -critical Name_Server_Statistics/other_query_failures=100 \
        -warning  Name_Server_Statistics/queries_resulted_in_SERVFAIL=50 \
        -warning  Name_Server_Statistics/other_query_failures=50

Erzeugt eine Warnung, wenn der lokale C<named> in letzter Zeit durchschnittlich
mindestens tausend Anfragen pro Sekunde beantworten musste oder durchschnittlich
mehr als fnfzig Anfragen pro Sekunde mit einem Fehler (insb. C<ServFail>)
beantworten musste bzw. einen kritischen Alarm, falls es mehr als hundert
Fehler-Anfragen pro Sekunde waren.

=head1 BESCHREIBUNG

Das Script bittet C<named> mittels eines C<rndc stats>-Kommandos, aktuelle
Statistikwerte in eine Stats-Datei zu loggen, vgl. "DNS and BIND", 4th Edition,
Seiten 191 ff.
Es vergleicht sodann diese Statistikwerte mit den beim vorangegangenen Aufruf
geschriebenen und errechnet anhand der ebenfalls mitprotokollierten Zeitstempel
die Durchschnittswerte pro Sekunde, um diese anschlieend mit den angegebenen
Schwellenwerten zu vergleichen und bei berschreitungen entsprechend
Alarmzustnde zu generieren.

Geht irgendetwas unerwartet schief, wird mindestens ein C<Unknown>-Zustand
erzeugt.

Welche Statistik-Werte es gibt, ist von der BIND-Version abhngig.
Einen berblick kann man sich verschaffen, indem man das Plugin mit L</-debug>
aufruft.

=head1 OPTIONEN

=over 4

=item -critical Variable=Schwellenwert

zur Definition der Schwellenwerte fr kritische Alarmzustnde,
vgl. L<Synopse|/SYNOPSE>

=item -warning Variable=Schwellenwert

zur Definition der Schwellenwerte fr Warnzustnde, vgl. L<Synopse|/SYNOPSE>

=item -decimals Anzahl

zur Festlegung, mit wie vielen Dezimalstellen Fliekommawerte (z. B. die
Durchschnittswerte pro Sekunde) ausgegeben werden sollen; Default: C<0>

=item -stats-command Kommando

um festzulegen, mit welchem Kommando C<named> gebeten werden soll, aktuelle
Statistikwerte auszugeben; Default: C</usr/sbin/rndc stats>

Um dem Kommando Optionen mitzugeben, muss die Option mehrfach verwendet werden,
z. B.

	-stats-command /usr/sbin/rndc -stats-command stats

=item -stats-file Dateipfad

legt den Namen der Stats-Datei fest; Default: F</var/named/var/log/named.stats>

=item -max-wait Sekunden

um festzulegen, wie lange wir C<named> maximal Zeit geben mchten, um
die angeforderten Statistikwerte in die Stats-Datei zu schreiben; Default: 10.

=item -debug

um zu Debugging-Zwecken auf der Standardausgabe genauere
Informationen ber die gemessenen Werte zu bekommen

=item -help

=item -?

um (nur) diese Dokumentation anzeigen zu lassen

=back

=head1 AUTOR

 Martin H. Sluka <fany@noris.net>
 fr die noris network AG
 RT#209019

=head1 BEISPIEL-DATEN

=head2 BIND < 9.5

    +++ Statistics Dump +++ (1112881539)
    success 2489125
    referral 88502
    nxrrset 15417
    nxdomain 200210
    recursion 0
    failure 2517999
    --- Statistics Dump --- (1112881539)

=head1 BIND 9.5

    +++ Statistics Dump +++ (1263390314)
    ++ Incoming Requests ++
                 1850157 QUERY
                  988195 UPDATE
    ++ Incoming Queries ++
                  754078 A
                     720 NS
                     416 CNAME
                  581642 SOA
                  158379 PTR
                   13822 MX
                     275 TXT
                    1583 AAAA
                  150809 TKEY
                    2875 IXFR
                   44058 AXFR
                  141500 ANY
    ++ Outgoing Queries ++
    [View: default]
                     146 A
                       2 NS
                     146 AAAA
    [View: _bind]
    ++ Name Server Statistics ++
                 2838352 IPv4 requests received
                  363826 requests with EDNS(0) received
                  328718 requests with TSIG received
                  198169 TCP requests received
                      60 auth queries rejected
                     807 transfer requests rejected
                 1540624 update requests rejected
                 2792229 responses sent
                  363826 responses with EDNS(0) sent
                  327077 responses with TSIG sent
                  633929 queries resulted in successful answer
                  858807 queries resulted in authoritative answer
                  793547 queries resulted in non authoritative answer
                  793547 queries resulted in referral answer
                   17025 queries resulted in nxrrset
                       1 queries resulted in SERVFAIL
                  207853 queries resulted in NXDOMAIN
                  150869 other query failures
                   46123 requested transfers completed
                     754 updates completed
                  211065 updates failed
                    4295 updates rejected due to prerequisite failure
    ++ Zone Maintenance Statistics ++
                   46629 IPv4 notifies sent
                      63 IPv6 notifies sent
    ++ Resolver Statistics ++
    [Common]
    [View: default]
                     212 IPv4 queries sent
                      82 IPv6 queries sent
                     208 IPv4 responses received
                       4 NXDOMAIN received
                      88 query retries
                      39 IPv4 NS address fetches
                      39 IPv6 NS address fetches
                       2 IPv4 NS address fetch failed
                      14 IPv6 NS address fetch failed
    [View: _bind]
    ++ Cache DB RRsets ++
    [View: default]
                      90 A
                      13 NS
                      49 AAAA
                       1 DS
                      13 RRSIG
                       1 NSEC
                       2 !AAAA
    [View: _bind]
    ++ Per Zone Query Statistics ++
    --- Statistics Dump --- (1263390314)
