#!/usr/bin/perl -T
# SPDX-License-Identifier: BSD-2-Clause

#------------------------------------------------------------------------------
# This is amavisd-agent, a demo program to display
# SNMP-like counters updated by amavis.
#
# Author: Mark Martinec <Mark.Martinec@ijs.si>
#
# Copyright (c) 2004-2014, 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:
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
# 2. 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.
#
# 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 views and conclusions contained in the software and documentation are
# those of the authors and should not be interpreted as representing official
# policies, either expressed or implied, of the Jozef Stefan Institute.

# (the above license is the 2-clause BSD license, also known as
#  a "Simplified 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 warnings;
no warnings 'uninitialized';

use Errno qw(ENOENT);
use Time::HiRes ();
use BerkeleyDB;

my($dbfile) = 'snmp.db';
my($db_home) =  # DB databases directory
  defined $ENV{'AMAVISD_DB_HOME'} ? $ENV{'AMAVISD_DB_HOME'} : '/var/lib/amavis/db';

my($wakeuptime) = 10;  # -w, sleep time in seconds, may be fractional
my($repeatcount);      # -c, repeat count (when defined)

use vars qw($VERSION);  $VERSION = 2.701;
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(" %7.1f %%", 100*$values{$k}/$values{$tot_k})
    }
    print " ($tot_k)";
  }
  print "\n";
}

sub p1_size($$@) {
  my($k,$avg,@tot_k) = @_;
  my($scale) = 1024*1024;
  printf("%-35s %6.0fMB %4.0fMB/h", $k, $values{$k}/$scale, $avg*3600/$scale);
  for my $tot_k (@tot_k) {
    if ($values{$tot_k} <= 0) {
      printf("  --- %%")
    } else {
      printf(" %5.1f %%", 100*$values{$k}/$values{$tot_k})
    }
    print " ($tot_k)";
  }
  print "\n";
}

sub p1_time($$$$) {
  my($k,$dv,$dcnt,$tot_k) = @_;
  printf("%-35s %6.0f s   %8s s/msg (%s)\n",
         $k, $values{$k}/1000,
         $dcnt < 1 ? "---" : sprintf("%7.3f",$dv/1000/$dcnt),
         $tot_k);
}

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,$msgcnt,$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,$msgcnt]);
  my($average,$dv,$dt,$dcnt); 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];
  $dcnt = $latest->[2] - $oldest->[2];
  if ($n < 2 || $dt < $hold_time/2) {
    $dt = $uptime; $dv = $val; $dcnt = $msgcnt;  # average since the start time
  }
  if ($dt > 0) { $average = $dv/$dt }
  ($average, $dv, $dt, $dcnt, $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
  my($normal_termination) = 0;
  $SIG{INT} = sub { die "\n" };  # do the END code block
  while (@ARGV) {
    my($opt) = shift @ARGV;
    my($val) = shift @ARGV;
    if ($opt eq '-w' && $val =~ /^\+?\d+(?:\.\d*)?\z/) { $wakeuptime = $val }
    elsif ($opt eq '-c' && $val =~ /^[+-]?\d+\z/) { $repeatcount = $val }
    else
      { die "Usage: $0 [-c <count>] [-w <wait-interval>]\n" }
  }
  my($stat,$key,$val);
  my($env,$db,$old_db_inode,@dbstat,$cursor);
  for (;;) {
    last  if defined $repeatcount && $repeatcount <= 0;
    @dbstat = stat("$db_home/$dbfile");
    my($errn) = @dbstat ? 0 : 0+$!;
    $errn==0 || $errn==ENOENT  or die "stat $db_home/$dbfile: $!";
    if (defined $db && $old_db_inode != $dbstat[1]) {
      $db->db_close==0 or die "BDB db_close error: $BerkeleyDB::Error $!";
      undef $db;
      printf STDERR ("Reopening snmp database %s/%s\n", $db_home,$dbfile);
    }
    if (!defined $db && $errn==0) {
      $old_db_inode = $dbstat[1];
      $env = BerkeleyDB::Env->new(
        -Home => $db_home, -Flags => DB_INIT_CDB | DB_INIT_MPOOL,
        -ErrFile => \*STDOUT, -Verbose => 1);
      defined $env or die "BDB no env: $BerkeleyDB::Error $!";
      $db = BerkeleyDB::Hash->new(-Filename => $dbfile, -Env => $env);
      defined $db or die "BDB no dbS 1: $BerkeleyDB::Error $!";
    }
    $| = 0;
    %values = (); %virus_by_name = ();
    %virus_by_os = (); %spam_by_os = (); %ham_by_os = ();
    my($now); my($eval_stat,$interrupt); $interrupt = '';
    if (!defined $db) {
      printf STDERR ("No snmp database %s/%s; waiting...\n", $db_home,$dbfile);
    } else {
      $repeatcount--  if defined $repeatcount && $repeatcount > 0;
      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|C64) (.*)\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});
      } elsif ($values{$k} =~ /^(?:INT|TIM) (.*)\z/) {
        $values{$k} = $1;
      } else {
        printf("%-15s %s\n", $k,$values{$k});
        delete($values{$k});
      }
    }
    my($msgcnt) = $values{'InMsgs'};
    for (sort keys %values) {
      my($avg,$dv,$dt,$dcnt,$n) =
        enqueue($_, $now, $values{$_}, $msgcnt, $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(.*?)Msgs/)     { p1($_,$avg,'Content'.$1.'Msgs') }
      elsif (/^Content(.*?)Msgs(.*)\z/) { p1($_,$avg,'InMsgs'.$2) }
      elsif (/^Content/)              { p1($_,$avg,'InMsgs') }
      elsif (/^OpsSql/)               { p1($_,$avg,'InMsgsRecips') }
      elsif (/^InMsgsSize/)           { p1_size($_,$avg,'InMsgsSize') }
      elsif (/^InMsgsRecipsLocal\z/)  { p1($_,$avg,'InMsgsRecips') }
      elsif (/^InMsgsRecips(.*)\z/)   { p1($_,$avg,'InMsgs'.$1) }
      elsif (/^InMsgsBounce./)        { p1($_,$avg,'InMsgsBounce') }
      elsif (/^(InMsgs|Ops)/)         { p1($_,$avg,'InMsgs') }
      elsif (/^OutMsgsSize\z/)        { p1_size($_,$avg,'InMsgsSize') }
      elsif (/^OutMsgsSize/)          { p1_size($_,$avg,'OutMsgsSize') }
      elsif (/^OutMsgs\z/)            { p1($_,$avg,'InMsgs') }
      elsif (/^Out/)                  { p1($_,$avg,'OutMsgs') }
      elsif (/^QuarMsgsSize\z/)       { p1_size($_,$avg,'InMsgsSize') }
      elsif (/^QuarMsgsSize/)         { p1_size($_,$avg,'QuarMsgsSize') }
      elsif (/^Quar/)                 { p1($_,$avg,'QuarMsgs') }
      elsif (/^LogEntries\z/)         { p1($_,$avg,'InMsgs') }
      elsif (/^Log/)                  { p1($_,$avg,'LogEntries') }
      elsif (/^GenMailIdRetries/)     { p1($_,$avg,'InMsgs') }
      elsif (/^PenPalsAttempts\z/)    { p1($_,$avg,'InMsgsRecipsLocal') }
      elsif (/^PenPalsHits\z/)        { p1($_,$avg,'PenPalsAttempts')}
      elsif (/^PenPalsHits./)         { p1($_,$avg,'PenPalsHits') }
      elsif (/^PenPals/)              { p1($_,$avg,'PenPalsAttempts') }
      elsif (/^SqlAddrSenderAttempts\z/) { p1($_,$avg,'InMsgs') }
      elsif (/^SqlAddrSender/)        { p1($_,$avg,'SqlAddrSenderAttempts') }
      elsif (/^SqlAddrRecipAttempts\z/)  { p1($_,$avg,'InMsgsRecips') }
      elsif (/^SqlAddrRecip/)         { p1($_,$avg,'SqlAddrRecipAttempts') }
      elsif (/^banned\.byOS/)         { p1($_,$avg,'InMsgs') }
      elsif (/^TimeElapsed/i)         { p1_time($_,$dv,$dcnt,'InMsgs') }
      else                            { p1($_,$avg,undef) }
    }
    for (sort { $values{$b}<=>$values{$a} } grep {/^OpsDecTyp/} keys %values) {
      my($avg,$dv,$dt,$dcnt,$n) =
        enqueue($_, $now, $values{$_}, $msgcnt, $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|C64) (.*)\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,$dcnt,$n) =
          enqueue($_, $now, $href->{$_}, $msgcnt, $avg_int);
        p2($_,$avg,$tot_k,$href);
      }
    }
    if (0) {   # disabled
      $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,$dcnt,$n) =
            enqueue($_, $now, $href->{$_}, $msgcnt, $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,$dcnt,$n) =
          enqueue($_, $now, $values{$_}, $msgcnt, $avg_int);
        p1($_,$avg,'InMsgs');
      }
    }
    $| = 1;
    last  if defined $repeatcount && $repeatcount <= 0;
    Time::HiRes::sleep($wakeuptime)  if $wakeuptime > 0;
  } # forever
  $normal_termination = 1;

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"  if !$normal_termination;
}
