#!/usr/bin/perl -T

#------------------------------------------------------------------------------
# This is amavisd-agent, a demo program to display
# SNMP-like counters updated by amavisd-new.
#
# Author: Mark Martinec <mark.martinec@ijs.si>
# Copyright (C) 2004  Mark Martinec,  All Rights Reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
#   this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
# * Neither the name of the author, nor the name of the "Jozef Stefan"
#   Institute, nor the names of contributors may be used to endorse or
#   promote products derived from this software without specific prior
#   written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
# OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#(the license above is the new BSD license, and pertains to this program only)
#
# Patches and problem reports are welcome.
# The latest version of this program is available at:
#   http://www.ijs.si/software/amavisd/
#------------------------------------------------------------------------------

use strict;
use re 'taint';

use Time::HiRes ();
use BerkeleyDB;

use vars qw($VERSION);  $VERSION = 2.401;
use vars qw(%values %virus_by_name);
use vars qw(%virus_by_os %spam_by_os %ham_by_os);
use vars qw(%history $avg_int $uptime);

$avg_int = 5*60;  # 5 minute interval

sub p1($$@) {
  my($k,$avg,@tot_k) = @_;
  printf("%-35s %6d %6.0f/h", $k, $values{$k}, $avg*3600);
  for my $tot_k (@tot_k) {
    if ($values{$tot_k} <= 0) {
      printf("    --- %%")
    } else {
      printf(" %6.1f %%", 100*$values{$k}/$values{$tot_k})
    }
    print " ($tot_k)";
  }
  print "\n";
}

sub p2($$$$) {
  my($k,$avg,$tot_k,$href) = @_;
  if ($values{$tot_k} > 0) {
    printf("%-35s %6d %6.0f/h %6.1f %% (%s)\n",
          $k, $href->{$k}, $avg*3600, 100*$href->{$k}/$values{$tot_k}, $tot_k);
  }
}

sub enqueue($$$$) {
  my($name,$now,$val,$hold_time) = @_;
  if (ref $history{$name} ne 'ARRAY') { $history{$name} = [] }
  my($oldest_useful);
  for my $j (0..$#{$history{$name}}) {
    if ($history{$name}->[$j][0] + $hold_time >= $now)
      { $oldest_useful = $j; last }
  }
  if (defined $oldest_useful) {
    @{$history{$name}} =
      @{$history{$name}}[$oldest_useful..$#{$history{$name}}];
  }
  push(@{$history{$name}}, [$now,$val]);
  my($average,$dv,$dt); my($n) = scalar(@{$history{$name}});
  my($oldest) = $history{$name}->[0];
  my($latest) = $history{$name}->[$n-1];
  $dt = $latest->[0] - $oldest->[0];  $dv = $latest->[1] - $oldest->[1];
  if ($n < 2 || $dt < $hold_time/2) {
    $dt = $uptime; $dv = $val;  # average since the start time
  }
  if ($dt > 0) { $average = $dv/$dt }
  ($average, $dv, $dt, $n);
}

sub fmt_ticks($) {
  my($t) = @_;
  my($hh)= $t % 100; $t = int($t/100);
  my($s) = $t % 60;  $t = int($t/60);
  my($m) = $t % 60;  $t = int($t/60);
  my($h) = $t % 24;  $t = int($t/24);
  my($d) = $t;
  sprintf("%d days, %d:%02d:%02d.%02d", $d,$h,$m,$s,$hh);
};

# main program starts here
  $SIG{INT} = sub { die "\n" };  # do the END code block
  my($env) = BerkeleyDB::Env->new(
    '-Home'=>'/var/amavis/db', '-Flags'=> DB_INIT_CDB | DB_INIT_MPOOL);
  defined $env or die "BDB no env: $BerkeleyDB::Error $!";
  my($db) = BerkeleyDB::Hash->new(
    '-Filename'=>'snmp.db', '-Flags'=>DB_RDONLY, '-Env'=>$env );
  defined $db or die "BDB no dbS 1: $BerkeleyDB::Error $!";
  my($cursor);

  $| = 1;
  my($stat,$key,$val);
  for (;;) {
    %values = (); %virus_by_name = ();
    %virus_by_os = (); %spam_by_os = (); %ham_by_os = ();
    my($now); my($eval_stat,$interrupt); $interrupt = '';
    print "\n\n";
    { my($h1) = sub { $interrupt = $_[0] };
      local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
      eval {
        $cursor = $db->db_cursor;  # obtain read lock
        defined $cursor or die "db_cursor error: $BerkeleyDB::Error";
        $now = Time::HiRes::time;
        while ( ($stat=$cursor->c_get($key,$val,DB_NEXT)) == 0 ) {
          if ($key =~ /^(virus\.byname\..*)\z/s)  { $virus_by_name{$1} = $val }
          elsif ($key =~ /^(virus\.byOS\..*)\z/s) { $virus_by_os{$1} = $val }
          elsif ($key =~ /^(ham\.byOS\..*)\z/s)   { $ham_by_os{$1} = $val }
          elsif ($key =~ /^(?:spam|spammy)\.byOS\.(.*)\z/s)
                                         { $spam_by_os{"spam.byOS.$1"} = $val }
          else { $values{$key} = $val }
        }
        $stat==DB_NOTFOUND  or die "c_get: $BerkeleyDB::Error $!";
        $cursor->c_close==0 or die "c_close error: $BerkeleyDB::Error";
        $cursor = undef;
      };
      $eval_stat = $@;
      if (defined $db) {
        $cursor->c_close  if defined $cursor;  # unlock, ignoring status
        $cursor = undef;
      }
    }
    if ($interrupt ne '') { kill($interrupt,$$) }  # resignal
    elsif ($eval_stat ne '') { chomp($eval_stat); die "BDB $eval_stat\n" }
    for my $k (sort keys %values) {
      if ($values{$k} =~ /^C32 (.*)\z/) {
        $values{$k} = $1;
      } elsif ($k eq 'sysUpTime' && $values{$k} =~ /^INT (.*)\z/) {
        $uptime = $now - $1; my($ticks) = int($uptime*100);
        printf("%-15s %s %s (%s)\n",
               $k,'Timeticks', $ticks, fmt_ticks($ticks));
        delete($values{$k});
      } else {
        printf("%-15s %s\n", $k,$values{$k});
        delete($values{$k});
      }
    }
    for (sort keys %values) {
      my($avg,$dv,$dt,$n) = enqueue($_, $now, $values{$_}, $avg_int);
      if    (/^OpsDecTyp/)    {}  # later
      elsif (/^CacheHitsVirusMsgs$/)  { p1($_,$avg,'ContentVirusMsgs') }
      elsif (/^CacheHitsBannedMsgs$/) { p1($_,$avg,'ContentBannedMsgs') }
      elsif (/^CacheHitsSpamMsgs$/)   { p1($_,$avg,'ContentSpamMsgs') }
      elsif (/^Cache/)        { p1($_,$avg,'CacheAttempts') }
      elsif (/^Content/)      { p1($_,$avg,'InMsgs') }
      elsif (/^Quar/)         { p1($_,$avg,'QuarMsgs') }
      elsif (/^OpsSql/)       { p1($_,$avg,'InMsgsRecips') }
      elsif (/^(InMsgs|Ops)/) { p1($_,$avg,'InMsgs') }
      elsif (/^Out/)          { p1($_,$avg,'OutMsgs') }
      elsif (/^SqlAddrSender/){ p1($_,$avg,'SqlAddrSender') }
      elsif (/^SqlAddrRecip/) { p1($_,$avg,'SqlAddrRecip') }
      else                    { p1($_,$avg,undef) }
    }
    for (sort { $values{$b}<=>$values{$a} } grep {/^OpsDecTyp/} keys %values) {
      my($avg,$dv,$dt,$n) = enqueue($_, $now, $values{$_}, $avg_int);
      p1($_,$avg,'InMsgs');
    }
    for my $href (\%virus_by_name,\%virus_by_os,\%spam_by_os,\%ham_by_os) {
      for (keys %$href) { $href->{$_} = $1  if $href->{$_} =~ /^C32 (.*)\z/ }
    }
    for my $href (\%virus_by_os,\%spam_by_os,\%ham_by_os) {
      for (keys %$href) {
        /^[a-zA-Z]+\.byOS\.(.*)\z/; my($os) = $1;
        $values{"all.byOS.$os"} += $href->{$_};
      }
    }
    my($separated) = 0;
    for my $pair ([\%virus_by_name, 'ContentVirusMsgs',],
                  [\%virus_by_os,   'ContentVirusMsgs',],
                  [\%spam_by_os,    'ContentSpamMsgs', ],
                  [\%ham_by_os,     'ContentCleanMsgs' ] ) {
      my($href,$tot_k) = @$pair;
      for (sort {$href->{$b} <=> $href->{$a}} keys %$href) {
        if (!$separated) { print "\n"; $separated = 1 }
        my($avg,$dv,$dt,$n) = enqueue($_, $now, $href->{$_}, $avg_int);
        p2($_,$avg,$tot_k,$href);
      }
    }
    $separated = 0;
    for my $href (\%virus_by_os, \%spam_by_os, \%ham_by_os) {
      for (sort {$href->{$b} <=> $href->{$a}} keys %$href) {
        if (!$separated) { print "\n"; $separated = 1 }
        my($avg,$dv,$dt,$n) = enqueue($_, $now, $href->{$_}, $avg_int);
        /^[a-zA-Z]+\.byOS\.(.*)\z/; my($os) = $1;
        p2($_,$avg,"all.byOS.$os",$href);
      }
    }
    $separated = 0;
    for (sort { $values{$b}<=>$values{$a} }
              grep {/^all\.byOS\./} keys %values) {
      if (!$separated) { print "\n"; $separated = 1 }
      my($avg,$dv,$dt,$n) = enqueue($_, $now, $values{$_}, $avg_int);
      p1($_,$avg,'InMsgs');
    }
    sleep 10;
#   Time::HiRes::sleep 0.5;
  }

END {
  if (defined $db) {
    $cursor->c_close  if defined $cursor;  # ignoring status
    $db->db_close==0 or die "BDB db_close error: $BerkeleyDB::Error $!";
  }
  print STDERR "exited\n";
}
