File: //usr/share/perl5/vendor_perl/Amavis/DB/SNMP.pm
# SPDX-License-Identifier: GPL-2.0-or-later
package Amavis::DB::SNMP;
use strict;
use re 'taint';
use warnings;
use warnings FATAL => qw(utf8 void);
no warnings 'uninitialized';
# use warnings 'extra'; no warnings 'experimental::re_strict'; use re 'strict';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.412';
@ISA = qw(Exporter);
}
use BerkeleyDB;
use MIME::Base64;
use Time::HiRes ();
use Amavis::Conf qw(:platform $myversion $nanny_details_level);
use Amavis::Util qw(ll do_log do_log_safe
snmp_initial_oids snmp_counters_get
add_entropy fetch_entropy_bytes);
# open existing databases (called by each child process)
#
sub new {
my($class,$db_env) = @_; $! = 0; my $env = $db_env->get_db_env;
defined $env or die "BDB get_db_env (dbS/dbN): $BerkeleyDB::Error, $!.";
$! = 0; my $dbs = BerkeleyDB::Hash->new(-Filename=>'snmp.db', -Env=>$env);
defined $dbs or die "BDB no dbS: $BerkeleyDB::Error, $!.";
$! = 0; my $dbn = BerkeleyDB::Hash->new(-Filename=>'nanny.db',-Env=>$env);
defined $dbn or die "BDB no dbN: $BerkeleyDB::Error, $!.";
bless { 'db_snmp'=>$dbs, 'db_nanny'=>$dbn }, $class;
}
sub DESTROY {
my $self = $_[0];
local($@,$!,$_); my $myactualpid = $$;
if (defined($my_pid) && $myactualpid != $my_pid) {
do_log_safe(5,"Amavis::DB::SNMP DESTROY skip, clone [%s] (born as [%s])",
$myactualpid, $my_pid);
} else {
do_log_safe(5,"Amavis::DB::SNMP DESTROY called");
for my $db_name ('db_snmp', 'db_nanny') {
my $db = $self->{$db_name};
if (defined $db) {
eval {
$db->db_close==0 or die "db_close: $BerkeleyDB::Error, $!."; 1;
} or do { $@ = "errno=$!" if $@ eq '' };
if ($@ ne '' && $@ !~ /\bDatabase is already closed\b/)
{ warn "[$myactualpid] BDB S+N DESTROY INFO ($db_name): $@" }
undef $db;
}
}
}
}
#sub lock_stat($) {
# my $label = $_[0];
# my $s = qx'/usr/local/bin/db_stat-4.2 -c -h /var/amavis/db | /usr/local/bin/perl -ne \'$a{$2}=$1 if /^(\d+)\s+Total number of locks (requested|released)/; END {printf("%d, %d\n",$a{requested}, $a{requested}-$a{released})}\'';
# do_log(0, "lock_stat %s: %s", $label,$s);
#}
# insert startup time SNMP entry, called from the master process at startup
# (a classical subroutine, not a method)
#
sub put_initial_snmp_data($) {
my $db = $_[0];
my($eval_stat,$interrupt); $interrupt = '';
{ my $cursor;
my $h1 = sub { $interrupt = $_[0] };
local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
eval { # ensure cursor will be unlocked even in case of errors or signals
$cursor = $db->db_cursor(DB_WRITECURSOR); # obtain write lock
defined $cursor or die "BDB S db_cursor: $BerkeleyDB::Error, $!.";
my $list_ref = snmp_initial_oids();
for my $obj (@$list_ref) {
my($key,$type,$val) = @$obj;
$cursor->c_put($key, sprintf("%s %s",$type,$val), DB_KEYLAST) == 0
or die "BDB S c_put: $BerkeleyDB::Error, $!.";
};
$cursor->c_close==0 or die "BDB S c_close: $BerkeleyDB::Error, $!.";
undef $cursor; 1;
} or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
$cursor->c_close if defined $cursor; # unlock, ignoring status
undef $cursor;
}; # restore signal handlers
if ($interrupt ne '') { kill($interrupt,$$) } # resignal, ignoring status
elsif (defined $eval_stat) {
chomp $eval_stat;
die "put_initial_snmp_data: BDB S $eval_stat\n";
}
}
sub update_snmp_variables {
my $self = $_[0];
do_log(5,"updating snmp variables in BDB");
my $snmp_var_names_ref = snmp_counters_get();
my($eval_stat,$interrupt); $interrupt = '';
if (defined $snmp_var_names_ref && @$snmp_var_names_ref) {
my $db = $self->{'db_snmp'}; my $cursor;
my $h1 = sub { $interrupt = $_[0] };
local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
eval { # ensure cursor will be unlocked even in case of errors or signals
$cursor = $db->db_cursor(DB_WRITECURSOR); # obtain write lock
defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!.";
for my $key (@$snmp_var_names_ref) {
my($snmp_var_name,$arg,$type) = ref $key ? @$key : ($key);
$type = 'C32' if !defined($type) || $type eq '';
if ($type eq 'C32' || $type eq 'C64') { # a counter
if (!defined($arg)) { $arg = 1 } # by default counter increments by 1
elsif ($arg < 0) { $arg = 0 } # counter is supposed to be unsigned
} elsif ($type eq 'TIM') { # TimeTicks
if ($arg < 0) { $arg = 0 } # non-decrementing
}
my($val,$flags); local($1);
my $stat = $cursor->c_get($snmp_var_name,$val,DB_SET);
if ($stat==0) { # exists, update it (or replace it)
if ($type eq 'C32' && $val=~/^C32 (\d+)\z/) { $val = $1+$arg }
elsif ($type eq 'C64' && $val=~/^C64 (\d+)\z/) { $val = $1+$arg }
elsif ($type eq 'TIM' && $val=~/^TIM (\d+)\z/) { $val = $1+$arg }
elsif ($type eq 'INT' && $val=~/^INT ([+-]?\d+)\z/) { $val = $arg }
elsif ($type=~/^(STR|OID)\z/ && $val=~/^\Q$type\E (.*)\z/) {
if ($snmp_var_name ne 'entropy') { $val = $arg }
else { # blend-in entropy
$val = $1; add_entropy($val, Time::HiRes::gettimeofday);
$val = fetch_entropy_bytes(18); # 18 bytes
$val = encode_base64($val,''); # 18*8/6 = 24 chars
$val =~ tr{+/}{-_}; # base64 -> RFC 4648 base64url [A-Za-z0-9-_]
}
}
else {
do_log(-2,"WARN: variable syntax? %s: %s, clearing",
$snmp_var_name,$val);
$val = 0;
}
$flags = DB_CURRENT;
} else { # create new entry
$stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!.";
$flags = DB_KEYLAST; $val = $arg;
}
my $fmt = $type eq 'C32' ? "%010d" : $type eq 'C64' ? "%020.0f"
: $type eq 'INT' ? "%010d" : undef;
# format for INT should really be %011d, but keep compatibility for now
my $str = defined($fmt) ? sprintf($fmt,$val) : $val;
$cursor->c_put($snmp_var_name, $type.' '.$str, $flags) == 0
or die "c_put: $BerkeleyDB::Error, $!.";
}
$cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!.";
undef $cursor; 1;
} or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
if (defined $db) {
$cursor->c_close if defined $cursor; # unlock, ignoring status
undef $cursor;
# if (!defined($eval_stat)) {
# my $stat; $db->db_sync(); # not really needed
# $stat==0 or warn "BDB S db_sync,status $stat: $BerkeleyDB::Error, $!.";
# }
}
}; # restore signal handlers
delete $self->{'cnt'};
if ($interrupt ne '') { kill($interrupt,$$) } # resignal, ignoring status
elsif (defined $eval_stat) {
chomp $eval_stat;
die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
die "update_snmp_variables: BDB S $eval_stat\n";
}
}
sub read_snmp_variables {
my($self,@snmp_var_names) = @_;
my($eval_stat,$interrupt); $interrupt = '';
my $db = $self->{'db_snmp'}; my $cursor; my(@values);
{ my $h1 = sub { $interrupt = $_[0] };
local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
eval { # ensure cursor will be unlocked even in case of errors or signals
$cursor = $db->db_cursor; # obtain read lock
defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!.";
for my $cname (@snmp_var_names) {
my $val; my $stat = $cursor->c_get($cname,$val,DB_SET);
push(@values, $stat==0 ? $val : undef);
$stat==0 || $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!.";
}
$cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!.";
undef $cursor; 1;
} or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
if (defined $db) {
$cursor->c_close if defined $cursor; # unlock, ignoring status
undef $cursor;
}
}; # restore signal handlers
if ($interrupt ne '') { kill($interrupt,$$) } # resignal, ignoring status
elsif (defined $eval_stat) {
chomp $eval_stat;
die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
die "read_snmp_variables: BDB S $eval_stat\n";
}
for my $val (@values) {
if (!defined($val)) {} # keep undefined
elsif ($val =~ /^(?:C32|C64) (\d+)\z/) { $val = 0+$1 }
elsif ($val =~ /^(?:INT) ([+-]?\d+)\z/) { $val = 0+$1 }
elsif ($val =~ /^(?:STR|OID) (.*)\z/) { $val = $1 }
else { do_log(-2,"WARN: counter syntax? %s", $val); undef $val }
}
\@values;
}
sub register_proc {
my($self, $details_level, $reset_timestamp, $state, $task_id) = @_;
my $eval_stat; my $interrupt = '';
if (!defined($state) || $details_level <= $nanny_details_level) {
$task_id = '' if !defined $task_id;
my $db = $self->{'db_nanny'}; my $key = sprintf("%05d",$$);
my $cursor; my $val;
my $h1 = sub { $interrupt = $_[0] };
local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
eval { # ensure cursor will be unlocked even in case of errors or signals
$cursor = $db->db_cursor(DB_WRITECURSOR); # obtain write lock
defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!.";
my $stat = $cursor->c_get($key,$val,DB_SET);
$stat==0 || $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!.";
if ($stat==0 && !defined $state) { # remove existing entry
$cursor->c_del==0 or die "c_del: $BerkeleyDB::Error, $!.";
} elsif (defined $state) { # add new, or update existing entry
my $timestamp; local($1);
# keep its timestamp when updating existing record
$timestamp = $1 if $stat==0 && $val=~/^(\d+(?:\.\d*)?) /s;
$timestamp = sprintf("%014.3f", Time::HiRes::time)
if !defined($timestamp) || $reset_timestamp;
my $new_val = sprintf("%s %-14s", $timestamp, $state.$task_id);
$cursor->c_put($key, $new_val,
$stat==0 ? DB_CURRENT : DB_KEYLAST ) == 0
or die "c_put: $BerkeleyDB::Error, $!.";
}
$cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!.";
undef $cursor; 1;
} or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
if (defined $db) {
$cursor->c_close if defined $cursor; # unlock, ignoring status
undef $cursor;
# if (!defined($eval_stat)) {
# my $stat = $db->db_sync(); # not really needed
# $stat==0 or warn "BDB N db_sync,status $stat: $BerkeleyDB::Error, $!.";
# }
}
}; # restore signal handlers
if ($interrupt ne '') {
kill($interrupt,$$); # resignal, ignoring status
} elsif (defined $eval_stat) {
chomp $eval_stat;
do_log_safe(5, "register_proc: BDB N %s", $eval_stat);
die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
die "register_proc: BDB N $eval_stat\n";
}
}
1;