1#!/usr/local/bin/perl -T
2
3#------------------------------------------------------------------------------
4# This program implements a SNMP AgentX (RFC 2741) subagent for amavisd-new.
5#
6# Author: Mark Martinec <Mark.Martinec@ijs.si>
7#
8# Copyright (c) 2009-2014, Mark Martinec
9# All rights reserved.
10#
11# Redistribution and use in source and binary forms, with or without
12# modification, are permitted provided that the following conditions
13# are met:
14# 1. Redistributions of source code must retain the above copyright notice,
15#    this list of conditions and the following disclaimer.
16# 2. Redistributions in binary form must reproduce the above copyright notice,
17#    this list of conditions and the following disclaimer in the documentation
18#    and/or other materials provided with the distribution.
19#
20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
24# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30# POSSIBILITY OF SUCH DAMAGE.
31#
32# The views and conclusions contained in the software and documentation are
33# those of the authors and should not be interpreted as representing official
34# policies, either expressed or implied, of the Jozef Stefan Institute.
35
36# (the above license is the 2-clause BSD license, also known as
37#  a "Simplified BSD License", and pertains to this program only)
38#
39# Patches and problem reports are welcome.
40# The latest version of this program is available at:
41#   http://www.ijs.si/software/amavisd/
42#------------------------------------------------------------------------------
43
44package AmavisAgent;
45
46use strict;
47use re 'taint';
48use warnings;
49use warnings FATAL => qw(utf8 void);
50no warnings 'uninitialized';
51
52use Errno qw(ESRCH ENOENT EACCES EEXIST);
53use POSIX ();
54use Time::HiRes ();
55use IO::File qw(O_RDONLY O_WRONLY O_RDWR O_CREAT O_EXCL);
56use Unix::Syslog qw(:macros :subs);
57use BerkeleyDB;
58
59use vars qw($VERSION);  $VERSION = 1.007;
60
61use vars qw($myversion $myproduct_name $myversion_id $myversion_date);
62$myproduct_name = 'amavis-agentx';
63$myversion_id = '1.7'; $myversion_date = '20140127';
64$myversion = "$myproduct_name-$myversion_id ($myversion_date)";
65my($agent_name) = $myproduct_name;
66
67use vars qw($syslog_ident $syslog_facility);
68$syslog_ident = $myproduct_name;
69$syslog_facility = LOG_MAIL;
70
71my($db_home) =  # DB databases directory
72  defined $ENV{'AMAVISD_DB_HOME'} ? $ENV{'AMAVISD_DB_HOME'} : '/var/amavis/db';
73
74my($mta_queue_dir);
75
76my($top) = '1.3.6.1.4.1.15312.2.1';
77my(@databases) = (
78  { root_oid_str => "$top.1",     name => 'am.snmp',     file => 'snmp.db'  },
79  { root_oid_str => "$top.2",     name => 'am.nanny',    file => 'nanny.db' },
80  { root_oid_str => "$top.3.1.1", name => 'pf.maildrop', file => 'maildrop',
81                                  ttl => 18 },
82  { root_oid_str => "$top.3.1.2", name => 'pf.incoming', file => 'incoming',
83                                  ttl => 18 },
84  { root_oid_str => "$top.3.1.3", name => 'pf.active',   file => 'active',
85                                  ttl => 18 },
86  { root_oid_str => "$top.3.1.4", name => 'pf.deferred', file => 'deferred',
87                                  ttl => 18 },
88);
89
90# 1.3.6.1.4.1.15312        enterprises . Jozef Stefan Institute
91# 1.3.6.1.4.1.15312.2      amavisd-new
92# 1.3.6.1.4.1.15312.2.1    amavisd-new SNMP
93# 1.3.6.1.4.1.15312.2.1.1  amavisd-new Statistics
94# 1.3.6.1.4.1.15312.2.1.2  amavisd-new Process status
95# 1.3.6.1.4.1.15312.2.1.3  amavisd-new (a view into MTA queue sizes)
96# 1.3.6.1.4.1.15312.2.2    amavisd-new LDAP Elements
97
98
99my($log_level) = 0;
100my($daemonize) = 1;
101my($pid_filename);  # e.g. "/var/run/amavisd-snmp-subagent.pid";
102
103my($pid_file_created) = 0;
104my($syslog_open) = 0;
105my($num_proc_gone) = 0;
106
107# geometic progression, rounded,
108#   common ratio = exp((ln(60)-ln(1))/6) = 1.97860
109my(@age_slots) = (
110  0.1,    0.2,    0.5,
111  1,      2,      4,      8,      15,      30,        # seconds
112  1*60,   2*60,   4*60,   8*60,   15*60,   30*60,     # minutes
113  1*3600, 2*3600, 4*3600, 8*3600, 15*3600, 30*3600);  # hours
114
115
116package AmavisVariable;
117
118sub new    { my($class) = @_; bless [(undef) x 7], $class }
119sub oid    { my($self)=shift; !@_ ? $self->[0] : ($self->[0]=shift) }
120sub oidstr { my($self)=shift; !@_ ? $self->[1] : ($self->[1]=shift) }
121sub name   { my($self)=shift; !@_ ? $self->[2] : ($self->[2]=shift) }
122sub type   { my($self)=shift; !@_ ? $self->[3] : ($self->[3]=shift) }
123sub suffix { my($self)=shift; !@_ ? $self->[4] : ($self->[4]=shift) }
124sub value  { my($self)=shift; !@_ ? $self->[5] : ($self->[5]=shift) }
125sub next   { my($self)=shift; !@_ ? $self->[6] : ($self->[6]=shift) }
126
127
128package AmavisAgent;
129
130use NetSNMP::OID;
131use NetSNMP::ASN qw(:all);
132use NetSNMP::agent qw(:all);
133use NetSNMP::default_store qw(:all);
134
135my(%oidstr_to_obj);
136my(@oid_sorted_list);
137my($keep_running) = 1;
138my(%variables);
139
140my(%asn_name_to_type) = (
141  'C32' => ASN_COUNTER,
142  'C64' => ASN_COUNTER64,
143  'G32' => ASN_GAUGE,
144  'INT' => ASN_INTEGER,
145  'I64' => ASN_INTEGER64,
146  'U32' => ASN_UNSIGNED,
147  'U64' => ASN_UNSIGNED64,
148  'STR' => ASN_OCTET_STR,
149  'OID' => ASN_OBJECT_ID,
150  'TIM' => ASN_TIMETICKS,
151);
152
153my(%asn_type_to_full_name) = (
154  ASN_COUNTER,    'Counter32',
155  ASN_COUNTER64,  'Counter64',
156  ASN_GAUGE,      'Gauge32',
157  ASN_INTEGER,    'Integer32',
158#                 'IpAddress',
159  ASN_INTEGER64,  'Integer64',
160  ASN_UNSIGNED,   'Unsigned32',
161  ASN_UNSIGNED64, 'Unsigned64',
162  ASN_OCTET_STR,  'DisplayString',
163  ASN_OBJECT_ID,  'OBJECT IDENTIFIER',
164  ASN_TIMETICKS,  'TimeTicks',
165);
166
167sub do_log($$;@) {
168  my($level,$errmsg,@args) = @_;
169  if ($level <= $log_level) {
170    # treat $errmsg as sprintf format string if additional arguments provided
171    if (@args) { $errmsg = sprintf($errmsg,@args) }
172    if (!$syslog_open) {
173      print STDERR $errmsg."\n";  # ignoring I/O status
174    } else {
175      my($prio) = $level <= -2 ? LOG_ERR
176                : $level <= -1 ? LOG_WARNING
177                : $level <=  0 ? LOG_NOTICE
178                : $level <=  1 ? LOG_INFO
179                :                LOG_DEBUG;
180      syslog(LOG_INFO, "%s", $errmsg);
181    }
182  }
183}
184
185# Returns the smallest defined number from the list, or undef
186sub min(@) {
187  my($r) = @_ == 1 && ref($_[0]) ? $_[0] : \@_;  # accept list, or a list ref
188  my($m);  for (@$r) { $m = $_  if defined $_ && (!defined $m || $_ < $m) }
189  $m;
190}
191
192# Returns the largest defined number from the list, or undef
193sub max(@) {
194  my($r) = @_ == 1 && ref($_[0]) ? $_[0] : \@_;  # accept list, or a list ref
195  my($m);  for (@$r) { $m = $_  if defined $_ && (!defined $m || $_ > $m) }
196  $m;
197}
198
199# Return untainted copy of a string (argument can be a string or a string ref)
200sub untaint($) {
201  no re 'taint';
202  my($str);
203  if (defined($_[0])) {
204    local($1); # avoid Perl taint bug: tainted global $1 propagates taintedness
205    $str = $1  if (ref($_[0]) ? ${$_[0]} : $_[0]) =~ /^(.*)\z/s;
206  }
207  $str;
208}
209
210sub declare_variable($$;$$$) {
211  my($oid_str,$name, $typename,$instance_lo,$instance_hi) = @_;
212  $typename = 'C32'  if !defined $typename;
213  $instance_lo = 0               if !defined $instance_lo;
214  $instance_hi = $instance_lo    if !defined $instance_hi;
215  $instance_hi = $instance_lo    if $instance_hi < $instance_lo;
216  my($type) = $asn_name_to_type{$typename};
217  for my $ind ($instance_lo .. $instance_hi) {
218    my($full_oid_str) = sprintf("%s.%d", $oid_str,$ind);
219  # do_log(5, "declaring variable %s, %s", $full_oid_str, $name);
220    my($var) = AmavisVariable->new;
221    $var->oidstr($full_oid_str);
222  # $var->oid(NetSNMP::OID->new($full_oid_str));  # later
223    $var->type($type);
224  # $var->suffix($suffix);
225    $var->name("$name.$ind");
226    if (!exists $variables{"$name.$ind"}) {
227      $variables{"$name.$ind"} = $var;
228    } else {
229      # allow an amavisd variable name to map to multiple SNMP variables
230      if (ref $variables{"$name.$ind"} ne 'ARRAY') {
231        $variables{"$name.$ind"} = [ $variables{"$name.$ind"} ];
232      }
233      push(@{$variables{"$name.$ind"}}, $var);
234    }
235  }
236}
237
238sub set_variable_value($$) {
239  my($name, $value) = @_;
240  my($instance); local($1,$2);
241  if ($name =~ /^(.*)\.(\d+)/) { $name = $1; $instance = $2 }
242  $instance = 0  if !defined $instance;
243  my($v) = $variables{"$name.$instance"};
244  if (!defined($v)) {
245    do_log(5, "No such variable %s.%s", $name,$instance);
246  } else {
247    my(@var) = ref $v eq 'ARRAY' ? @$v : $v;
248    for my $var (@var) {
249      my($type) = $var->type;
250      if ($name =~ /^TimeElapsed/) { $value = $value/10 }  # ms -> 0.01s ticks
251      elsif ($type == ASN_COUNTER   || $type == ASN_GAUGE     ||
252             $type == ASN_INTEGER   || $type == ASN_UNSIGNED  ||
253             $type == ASN_TIMETICKS)  { $value = 0+$value }
254      elsif ($type == ASN_COUNTER64 || $type == ASN_INTEGER64 ||
255             $type == ASN_UNSIGNED64) { $value = sprintf("%1.0f",$value) }
256      elsif ($type == ASN_OCTET_STR)  { $value = "$value" }
257      $var->value($value);
258    }
259  }
260}
261
262sub reset_all_variable_values($) {
263  my($root_oid_str) = @_;
264  while (my($key,$v) = each(%variables)) {
265    my(@var) = ref $v eq 'ARRAY' ? @$v : $v;
266    for my $var (@var) {
267      if (!defined($root_oid_str) || $var->oidstr =~ /^\Q$root_oid_str\E\./) {
268        $var->value(undef);
269      }
270    }
271  }
272}
273
274sub dump_variables() {
275  for my $oid (@oid_sorted_list) {
276    my(@oidlist) = $oid->to_array;
277    my($oidstr) = join('.', @oidlist);
278    my($descr) = "";
279    my($suffix_sp) = join(' ', @oidlist[9 .. ($#oidlist-1)]);
280    my($var) = $oidstr_to_obj{$oidstr};
281    my($mib_type_name) = $asn_type_to_full_name{$var->type};
282    my($name) = $var->name;
283    $name =~ s/\.0\z//;
284    printf STDERR (<<'END', $name, $mib_type_name, $descr, $suffix_sp);
285%s OBJECT-TYPE
286    SYNTAX	%s
287    MAX-ACCESS	read-only
288    STATUS	current
289    DESCRIPTION
290      "%s"
291    ::= { amavis %s }
292
293END
294  }
295}
296
297sub init_data() {
298  while (my($name,$v) = each(%variables)) {
299    my(@var) = ref $v eq 'ARRAY' ? @$v : $v;
300    for my $var (@var) {
301      my($oidstr) = $var->oidstr;
302      $var->oid(NetSNMP::OID->new($oidstr));
303      $oidstr_to_obj{$oidstr} = $var;
304    }
305  }
306  @oid_sorted_list = sort { snmp_oid_compare($a,$b) }
307                      map { $_->oid }
308                      map { ref $_ eq 'ARRAY' ? @$_ : $_ } values(%variables);
309  # build a linked list of variable objects in OID sorted order
310  # to speed up sequential MIB traversal by getnext
311  my($prev_var);
312  for my $oid (@oid_sorted_list) {
313    my($oidstr) = join('.', $oid->to_array);
314    my($var) = $oidstr_to_obj{$oidstr};
315    $prev_var->next($var)  if defined $prev_var;
316    $prev_var = $var;
317  }
318}
319
320sub collect_all_db_data($$) {
321  my($database,$values) = @_;
322  my($dbfile) = $database->{file};
323  my(@dbstat) = stat("$db_home/$dbfile");
324  my($errn) = @dbstat ? 0 : 0+$!;
325  $errn==0 || $errn==ENOENT  or die "stat $db_home/$dbfile: $!";
326  if (defined $database->{db} && $database->{old_db_inode} != $dbstat[1]) {
327    $database->{db}->db_close==0
328      or die "BDB db_close error: $BerkeleyDB::Error $!";
329    undef $database->{db};
330    do_log(1, "Reopening snmp database %s/%s", $db_home,$dbfile);
331  }
332  if (!defined $database->{db} && $errn==0) {
333    reset_all_variable_values($database->{root_oid_str});
334    $database->{old_db_inode} = $dbstat[1];
335    $database->{env} = BerkeleyDB::Env->new(
336      -Home => $db_home, -Flags => DB_INIT_CDB | DB_INIT_MPOOL,
337      -ErrFile => \*STDOUT, -Verbose => 1);
338    defined $database->{env} or die "BDB no env: $BerkeleyDB::Error $!";
339    $database->{db} = BerkeleyDB::Hash->new(-Filename => $dbfile,
340                                          -Env => $database->{env});
341    defined $database->{db} or die "BDB no dbS 1: $BerkeleyDB::Error $!";
342  }
343  my($eval_stat,$interrupt); $interrupt = '';
344  if (!defined $database->{db}) {
345    do_log(1, "No snmp database %s/%s", $db_home,$dbfile);
346  } else {
347    my($stat,$key,$val);
348    my($h1) = sub { $interrupt = $_[0] };
349    local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
350    eval {
351      # be as quick as possible while a database is locked by a cursor
352      my($cursor) = $database->{db}->db_cursor;  # obtain read lock
353      $database->{cursor} = $cursor;
354      defined $cursor or die "db_cursor error: $BerkeleyDB::Error";
355      while ( ($stat=$cursor->c_get($key,$val,DB_NEXT)) == 0 ) {
356        $values->{$key} = $val;
357      }
358      $stat==DB_NOTFOUND  or die "c_get: $BerkeleyDB::Error $!";
359      $cursor->c_close==0 or die "c_close error: $BerkeleyDB::Error";
360      undef $database->{cursor};
361      1;
362    } or do {
363      $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
364    };
365    if (defined $database->{db}) {
366      # unlock, ignoring status
367      $database->{cursor}->c_close  if defined $database->{cursor};
368      undef $database->{cursor};
369    }
370  }
371  if ($interrupt ne '') { kill($interrupt,$$) }  # resignal
372  elsif ($eval_stat ne '') { chomp($eval_stat); die "BDB $eval_stat\n" }
373}
374
375sub count_files_in_postfix_dir($$);  # prototype
376sub count_files_in_postfix_dir($$) {
377  my($dir,$deadline) = @_;
378  local(*DIR); my($f); my($cnt) = 0; my($aborted) = 0;
379  if (!opendir(DIR,$dir)) {
380    do_log(-1, "Can't open directory %s: %s", $dir,$!);
381  } else {
382    while (defined($f = readdir(DIR))) {
383      next  if $f eq '.' || $f eq '..';
384      # Postfix uses one-character subdirs
385      if (length($f) == 1 && -d "$dir/$f") {
386        my($n,$abt) = count_files_in_postfix_dir("$dir/$f", $deadline);
387        $cnt += $n;
388        if ($abt) { $aborted = 1; last }
389      } else {
390        $cnt++;
391      }
392      if (defined $deadline && Time::HiRes::time > $deadline) {
393        $aborted = 1; last;
394      }
395    }
396    closedir(DIR) or die "Error closing directory $dir: $!";
397  }
398  ($cnt,$aborted);
399}
400
401sub update_data($) {
402  my($database) = @_;
403  do_log(3, "updating variables from %s", $database->{name});
404  my($start_time) = Time::HiRes::time;
405  if ($database->{name} =~ /^pf/) {
406    # not really a database file, just a 'view' into an MTA spool directory
407    my($dir) = $database->{file};
408    my($cnt,$aborted) =
409      count_files_in_postfix_dir("$mta_queue_dir/$dir", $start_time + 5);
410    my($var_name) = "MtaQueueEntries\u$dir";
411    set_variable_value($var_name, $cnt);
412    do_log(3, "mta queue: %s %d", $var_name,$cnt);
413    do_log(-1,"exceeded time limit on dir %s, aborted after %.1f s, ".
414              "count so far: %d",
415              $var_name, Time::HiRes::time - $start_time, $cnt)  if $aborted;
416  } elsif ($database->{file} eq 'snmp.db') {
417    my(%values); collect_all_db_data($database,\%values);
418    while (my($key,$val) = each(%values)) {
419      next if $key =~ /\.byOS\./s;
420      next if $key =~ /^virus\.byname\./s;
421      next if $key =~ /^(?:OpsDecType|OpsDecBy|OpsSql)/s;
422      next if $key =~ /^entropy/s;
423      next if $key =~ /^ContentBadHdr/ && $key !~ /^ContentBadHdrMsgs/;
424      local($1);
425      if ($val =~ /^(?:C32|C64) (.*)\z/) {
426        set_variable_value($key, 0+$1);
427      } elsif ($val =~ /^STR (.*)\z/) {
428        set_variable_value($key, "$1");
429      } elsif ($key eq 'sysUpTime' && $val =~ /^INT (.*)\z/) {
430        my($uptime) = $start_time - $1; my($ticks) = int($uptime*100);
431        set_variable_value($key, $ticks);
432      } elsif ($val =~ /^(?:G32|INT|I64|U32|U64|TIM) (.*)\z/) {
433        set_variable_value($key, 0+$1);
434      } elsif ($val =~ /^OID (.*)\z/) {
435        set_variable_value($key, $1);
436      } else {
437      # set_variable_value($key, $val);
438      }
439    }
440  } elsif ($database->{file} eq 'nanny.db') {
441    my(%values); collect_all_db_data($database,\%values);
442    my(%proc_timestamp, %proc_state, %proc_task_id);
443    while (my($key,$val) = each(%values)) {
444      local($1,$2);
445      if ($val !~ /^(\d+(?:\.\d*)?) (.*?) *\z/s) {
446        do_log(0, "Bad %s db entry: %s, %s", $database->{file},$key,$val);
447      } else {
448        $proc_timestamp{$key} = $1; my($task_id) = $2;
449        $proc_state{$key} = $1  if $task_id =~ s/^([^0-9])//;
450        $proc_task_id{$key} = $task_id;
451      }
452    }
453    my(@to_be_removed); my($num_proc_idle) = 0; my($num_proc_busy) = 0;
454    my(@num_proc_busy_by_age); my(%num_proc_busy_by_activity);
455    for my $pid (keys(%proc_timestamp)) {
456      my($idling) = $proc_task_id{$pid} eq '' &&
457                    $proc_state{$pid} =~ /^[. ]?\z/s;
458      my($age) = $start_time - $proc_timestamp{$pid};
459      my($n) = kill(0,$pid);  # test if the process is still there
460      if ($n == 0 && $! != ESRCH) {
461        do_log(-1, "Can't check the process %s: %s", $pid,$!);
462      } elsif ($n == 0) {  # ESRCH means there is no such process
463        push(@to_be_removed, $pid);  # process went away!
464      } elsif ($idling) {
465        $num_proc_idle++;
466      } else {  # busy for $age seconds
467        $num_proc_busy++;
468        $num_proc_busy_by_age[0]++;
469        my($j) = 1;
470        for my $t (@age_slots) {
471          if ($age >= $t) { $num_proc_busy_by_age[$j]++ }
472          $j++;
473        }
474        my($s) = $proc_state{$pid};
475        if ($s eq 'm' || $s eq 'd' || $s eq 'F') { $s = 'm' }
476        elsif ($s eq 'D' || $s eq 'V' || $s eq 'S') { }
477        else { $s = ' ' }
478        $num_proc_busy_by_activity{$s}++;
479      }
480    }
481    $num_proc_gone += scalar(@to_be_removed);
482    # all are gauges, except ProcGone
483    set_variable_value('ProcAll',  $num_proc_idle+$num_proc_busy);
484    set_variable_value('ProcIdle', $num_proc_idle);
485    set_variable_value('ProcBusy', $num_proc_busy);
486    set_variable_value('ProcGone', $num_proc_gone);  # counter!
487    for my $j (0..@age_slots) {  # age_slots start at 1, zero is in addition
488      set_variable_value("ProcBusy$j", $num_proc_busy_by_age[$j] || 0);
489    }
490    set_variable_value("ProcBusyTransfer",$num_proc_busy_by_activity{'m'}||0);
491    set_variable_value("ProcBusyDecode",  $num_proc_busy_by_activity{'D'}||0);
492    set_variable_value("ProcBusyVirus",   $num_proc_busy_by_activity{'V'}||0);
493    set_variable_value("ProcBusySpam",    $num_proc_busy_by_activity{'S'}||0);
494    set_variable_value("ProcBusyOther",   $num_proc_busy_by_activity{' '}||0);
495    if (@to_be_removed) {  # some processes no longer exist, update db
496      my($eval_stat,$interrupt); $interrupt = '';
497      my($h1) = sub { $interrupt = $_[0] };
498      local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
499      eval {
500        # obtain a write lock
501        my($cursor) = $database->{db}->db_cursor(DB_WRITECURSOR);
502        $database->{cursor} = $cursor;
503        defined $cursor or die "BDB db_cursor error: $BerkeleyDB::Error";
504        for my $key (@to_be_removed) {
505          my($val); my($stat) = $cursor->c_get($key,$val,DB_SET);
506          $stat==0 || $stat==DB_NOTFOUND
507            or die "BDB c_get: $BerkeleyDB::Error, $!.";
508          if ($stat==0) {  # remove existing entry
509            $cursor->c_del==0 or die "c_del: $BerkeleyDB::Error, $!.";
510          }
511        }
512        $cursor->c_close==0 or die "c_close error: $BerkeleyDB::Error";
513        undef $database->{cursor};
514        1;
515      } or do {
516        $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
517      };
518      if (defined $database->{db}) {
519        # unlock, ignoring status
520        $database->{cursor}->c_close  if defined $database->{cursor};
521        undef $database->{cursor};
522      }
523    }
524  }
525
526  my($now) = Time::HiRes::time;
527
528  my($elapsed) = $now - $start_time;
529  $elapsed = 0  if $elapsed < 0;  # clock jump?
530  my($ll) = $elapsed >= 30 ? -1 : $elapsed >= 5 ? 0 : $elapsed >= 1 ? 2 : 3;
531  do_log($ll, "updating %s took %.3f s", $database->{name}, $elapsed);
532
533  my($ttl_lower_bound) = 8*$elapsed;  # don't be a hog!
534  my($since_query) = $database->{last_query_timestamp};
535  $since_query = $now - $since_query  if defined $since_query;
536  if (defined $since_query && $elapsed > 4) {
537    # there is a chance that a SNMP client timed out on this query;
538    # stretch the next update period to allow one quick next response
539    # from cached data, assuming queries are at about regular intervals
540    $ttl_lower_bound = max($ttl_lower_bound, 1.5 * $since_query);
541  }
542  $ttl_lower_bound = min($ttl_lower_bound, 20*60);  # cap at 20 minutes
543  my($ttl) = $database->{ttl};
544  $ttl = 4  if !defined $ttl || $ttl <= 0;
545  if ($ttl < $ttl_lower_bound) {
546    $ttl = $ttl_lower_bound;
547    do_log(3, "postponing refresh on %s for another %.1f s%s",
548              $database->{name}, $ttl,
549              !defined $since_query ? ''
550                : sprintf(", %.1f s since query",$since_query) );
551  }
552  $database->{last_refreshed} = $now;
553  $database->{update_due_at} = $now + $ttl;
554}
555
556sub find_next_gt($$) {
557  my($x, $a_ref) = @_;
558  my($l, $u) = (0, $#$a_ref);
559  my $j;
560  while ($l <= $u) {
561    $j = $l + int(($u - $l)/2);  # good practices: avoids integer overflow
562    if ($a_ref->[$j] > $x) { $u = $j-1 } else { $l = $j+1 }
563  }
564  $l > $#$a_ref ? -1 : $l;
565}
566
567my($fast_poll) = 0;
568my($last_query_timestamp) = 0;
569
570sub snmp_handler($$$$) {
571  my($handler, $registration_info, $request_info, $requests) = @_;
572
573  my($now) = Time::HiRes::time;
574  my($dt) = $now - $last_query_timestamp;
575  if ($dt < 1.5) { $fast_poll = 1 } elsif ($dt > 4) { $fast_poll = 0 }
576  $last_query_timestamp = $now;
577
578  my($mode) = $request_info->getMode;
579  for (my $req=$requests; $req; $req=$req->next) {
580    my($oid_in_request) = $req->getOID;  # OID from a request
581    my($actual_oid); my($err); my($eom) = 0;
582    if ($mode == MODE_GET) {
583      $actual_oid = $oid_in_request;
584      do_log(5, "Get %s", $oid_in_request);
585    } elsif ($mode == MODE_GETBULK) {
586      # never happens, not registered for getbulk
587      do_log(2, "GetBulk %s", $oid_in_request);
588    } elsif ($mode == MODE_GETNEXT) {
589      if (!@oid_sorted_list) {
590        $eom = 1;  # end of MIB
591      } elsif ($oid_in_request < $oid_sorted_list[0]) {
592        $actual_oid = $oid_sorted_list[0];
593        $req->setOID($actual_oid);
594        do_log(4, "First: %s -> %s", $oid_in_request,$actual_oid);
595      } elsif ($oid_in_request > $oid_sorted_list[-1]) {
596        $eom = 1;  # end of MIB
597        do_log(4, "Last: %s", $oid_in_request);
598      } else {
599        # check first for a sequential traversal, likely faster
600        my($var) = $oidstr_to_obj{join('.', $oid_in_request->to_array)};
601        if ($var) {
602          my($next_var) = $var->next;
603          if (!$next_var) {
604            $eom = 1;  # end of MIB
605          } else {
606            $actual_oid = $next_var->oid;
607            $req->setOID($actual_oid);
608          }
609        }
610        if (!$err && !defined $actual_oid) {  # fall back to a binary search
611          do_log(5, "Using a binary search for %s", $oid_in_request);
612          my($ind) = find_next_gt($oid_in_request, \@oid_sorted_list);
613          if ($ind < 0) {
614            $eom = 1;  # end of MIB
615          } else {
616            $actual_oid = $oid_sorted_list[$ind];
617            $req->setOID($actual_oid);
618          }
619        }
620      }
621      do_log(5, "GetNext %s -> %s", $oid_in_request,
622                !defined $actual_oid ? 'undef' : $actual_oid);
623    } else {
624      do_log(0, "Unknown request %s", $oid_in_request);
625      $req->setError($request_info, SNMP_ERR_NOTWRITABLE); $err = 1;
626    }
627    if ($err) {
628      # already dealt with
629    } elsif ($eom || !defined $actual_oid) {  # end of MIB
630      # just silently not provide a value
631      do_log(5, "No more MIB beyond %s", $oid_in_request);
632    } else {
633      my($oid_str) = join('.', $actual_oid->to_array);
634      my($var) = $oidstr_to_obj{$oid_str};
635      if (!$var) {
636        $req->setError($request_info, SNMP_ERR_NOSUCHNAME);
637      } else {
638        # find out under which OID root the query falls
639        for my $database (@databases) {
640          next  if !$database->{registered};
641          my($root_oid_str) = $database->{root_oid_str};
642          if ($oid_str =~ /^\Q$root_oid_str\E\./) {
643            $database->{last_query_timestamp} = $now;
644            if (!defined($database->{update_due_at}) ||
645                Time::HiRes::time >=
646                  $database->{update_due_at} + ($fast_poll ? 4 : 0) ) {
647              # fast polling stretches time-to-update a bit, increasing
648              # chances of collecting consistent data from the same moment
649              update_data($database);  # stale MIB, needs updating
650            }
651          }
652        }
653        my($type, $value, $name) = ($var->type, $var->value, $var->name);
654        if (!defined $type) {
655          $req->setError($request_info, SNMP_ERR_BADVALUE);
656        } else {
657          if (!defined $value) {
658            if    ($type == ASN_OCTET_STR)  { $value = "" }
659            elsif ($type == ASN_OBJECT_ID)  { $value = "0" }
660            elsif ($type == ASN_COUNTER64 || $type == ASN_INTEGER64 ||
661                   $type == ASN_UNSIGNED64) { $value = "0" }
662            else { $value = 0 }
663          }
664          my($status) = $req->setValue($type,$value);
665          if (!$status) {
666            do_log(0, "setValue error: %s, %s, %s", $type,$name,$value);
667            $req->setError($request_info, SNMP_ERR_BADVALUE);
668          }
669        }
670      }
671    }
672  }
673  1;
674}
675
676sub daemonize() {
677  my($pid);
678  closelog(); $syslog_open = 0;
679
680  STDOUT->autoflush(1);
681  STDERR->autoflush(1);
682
683  # the first fork allows the shell to return and allows doing a setsid
684  eval { $pid = fork(); 1 }
685  or do {
686    my($eval_stat) = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
687    die "Error forking #1: $eval_stat";
688  };
689  defined $pid  or die "Can't fork #1: $!";
690  if ($pid) {  # parent process terminates here
691    POSIX::_exit(0);  # avoid END and destructor processing
692  }
693
694  # disassociate from a controlling terminal
695  my($pgid) = POSIX::setsid();
696  defined $pgid && $pgid >= 0 or die "Can't start a new session: $!";
697
698  # We are now a session leader. As a session leader, opening a file
699  # descriptor that is a terminal will make it our controlling terminal.
700  # The second fork makes us NOT a session leader. Only session leaders
701  # can acquire a controlling terminal, so we may now open up any file
702  # we wish without worrying that it will become a controlling terminal.
703
704  # second fork prevents from accidentally reacquiring a controlling terminal
705  eval { $pid = fork(); 1 }
706  or do {
707    my($eval_stat) = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
708    die "Error forking #2: $eval_stat";
709  };
710  defined $pid  or die "Can't fork #2: $!";
711  if ($pid) {  # parent process terminates here
712    POSIX::_exit(0);  # avoid END and destructor processing
713  }
714
715  # a daemonized child process, live long and prosper...
716  do_log(2, "Daemonized as process [%s]", $$);
717
718  chdir('/')  or die "Can't chdir to '/': $!";
719
720  openlog($syslog_ident, LOG_PID | LOG_NDELAY, $syslog_facility);
721  $syslog_open = 1;
722
723  close(STDIN)                or die "Can't close STDIN: $!";
724  close(STDOUT)               or die "Can't close STDOUT: $!";
725  open(STDIN,  '</dev/null')  or die "Can't open /dev/null: $!";
726  open(STDOUT, '>/dev/null')  or die "Can't open /dev/null: $!";
727  close(STDERR)               or die "Can't close STDERR: $!";
728  open(STDERR, '>&STDOUT')    or die "Can't dup STDOUT: $!";
729}
730
731sub usage() {
732  return <<"EOD";
733Usage:
734  $0 [options]
735
736  Options:
737   -V              show version, then exit
738   -h              show help, then exit
739   -f              stay in foreground
740   -d log_level    debugging level, 0..5, default 0
741   -P pid_file     a file name to receive a PID of a damonized process
742   -D db_home_dir  amavis database directory ($db_home),
743                     default AMAVISD_DB_HOME or /var/amavis/db
744EOD
745}
746
747
748# main program starts here
749
750  delete @ENV{'PATH', 'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
751  $SIG{INT}  = sub { die "interrupted\n" };  # do the END code block
752  $SIG{TERM} = sub { die "terminated\n" };   # do the END code block
753  $SIG{PIPE} = 'IGNORE';  # don't signal on a write to a widowed pipe
754
755  while (@ARGV >= 2 && $ARGV[0] =~ /^-[dDP]\z/ ||
756         @ARGV >= 1 && $ARGV[0] =~ /^-[hVf-]\z/) {
757    my($opt,$val);
758    $opt = shift @ARGV;
759    $val = shift @ARGV  if $opt !~ /^-[hVf-]\z/;  # these take no arguments
760    if ($opt eq '--') {
761      last;
762    } elsif ($opt eq '-h') {  # -h  (help)
763      die "$myversion\n\n" . usage();
764    } elsif ($opt eq '-V') {  # -V  (version)
765      die "$myversion\n";
766    } elsif ($opt eq '-f') {  # -f  (foreground)
767      $daemonize = 0;
768    } elsif ($opt eq '-d') {  # -d log_level
769      $log_level = 0+$val;
770    } elsif ($opt eq '-D') {  # -D db_home_dir, empty string turns off db use
771      $db_home = untaint($val)  if $val ne '';
772    } elsif ($opt eq '-P') {  # -P pid_file
773      $pid_filename = untaint($val)  if $val ne '';
774    } else {
775      die "Error in parsing command line options: $opt\n\n" . usage();
776    }
777  }
778  !@ARGV  or die "Unprocessed command line options: $ARGV[0]\n\n" . usage();
779
780  if (!defined $mta_queue_dir) {  # test for access to Postfix queue directory
781    local($ENV{PATH}) = '/usr/sbin:/usr/local/sbin:/opt/postfix/sbin';
782    $! = 0;
783    $mta_queue_dir = qx(postconf -h queue_directory);
784    if (!defined $mta_queue_dir) {
785      if ($! != 0) {
786        do_log(1, "no postfix (unable to run postconf command): $!");
787      } else {
788        do_log(1, "failed to execute \"postconf queue_directory\": $?");
789      }
790    } else {
791      chomp $mta_queue_dir;
792      if ($mta_queue_dir =~ /^\s*\z/) {
793        do_log(1, "unknown Postfix queue directory");
794        undef $mta_queue_dir;
795      } else {
796        do_log(2, "got a Postfix queue directory: %s", $mta_queue_dir);
797        my($dir) = "$mta_queue_dir/active";
798        local(*DIR);
799        if (!opendir(DIR,$dir)) {  # testing access
800          do_log(1, "can't open directory %s: %s", $dir,$!);
801          undef $mta_queue_dir;
802        } else {
803          closedir(DIR) or die "Error closing directory $dir: $!";
804        }
805      }
806    }
807  }
808
809  { # amavisd statistics MIB
810    my($r) = $databases[0]->{root_oid_str};
811    declare_variable("$r.1.1",     'sysDescr',      'STR');
812    declare_variable("$r.1.2",     'sysObjectID',   'OID');
813    declare_variable("$r.1.3",     'sysUpTime',     'TIM');
814    declare_variable("$r.1.4",     'sysContact',    'STR');
815    declare_variable("$r.1.5",     'sysName',       'STR');
816    declare_variable("$r.1.6",     'sysLocation',   'STR');
817    declare_variable("$r.1.7",     'sysServices',   'INT');
818
819    declare_variable("$r.2.1",     'InMsgs');                  # orig=x locl=x
820    declare_variable("$r.2.2",     'InMsgsInbound');           # orig=0 locl=1
821    declare_variable("$r.2.3",     'InMsgsOutbound');          # orig=1 locl=0
822    declare_variable("$r.2.4",     'InMsgsInternal');          # orig=1 locl=1
823    declare_variable("$r.2.5",     'InMsgsOriginating');       # orig=1 locl=x
824    declare_variable("$r.2.6",     'InMsgsOpenRelay');         # orig=0 locl=0
825
826    # these have duplicates at $r.{19..26}.1, except InMsgsStatusRelayed
827    declare_variable("$r.2.7",     'InMsgsStatusAccepted');    # 2xx, AM.PDP
828    declare_variable("$r.2.8",     'InMsgsStatusRelayed');     # 2xx, forward
829    declare_variable("$r.2.9",     'InMsgsStatusDiscarded');   # 2xx, no DSN
830    declare_variable("$r.2.10",    'InMsgsStatusNoBounce');    # 2xx, no DSN
831    declare_variable("$r.2.11",    'InMsgsStatusBounced');     # 2xx, DSN sent
832    declare_variable("$r.2.12",    'InMsgsStatusRejected');    # 5xx
833    declare_variable("$r.2.13",    'InMsgsStatusTempFailed');  # 4xx
834
835    declare_variable("$r.3.1",     'InMsgsSize',            'C64');
836    declare_variable("$r.3.2",     'InMsgsSizeInbound',     'C64');
837    declare_variable("$r.3.3",     'InMsgsSizeOutbound',    'C64');
838    declare_variable("$r.3.4",     'InMsgsSizeInternal',    'C64');
839    declare_variable("$r.3.5",     'InMsgsSizeOriginating', 'C64');
840    declare_variable("$r.3.6",     'InMsgsSizeOpenRelay',   'C64');
841
842    declare_variable("$r.4.1",     'InMsgsRecips');            # orig=x locl=x
843    declare_variable("$r.4.2",     'InMsgsRecipsInbound');     # orig=0 locl=1
844    declare_variable("$r.4.3",     'InMsgsRecipsOutbound');    # orig=1 locl=0
845    declare_variable("$r.4.4",     'InMsgsRecipsInternal');    # orig=1 locl=1
846    declare_variable("$r.4.5",     'InMsgsRecipsOriginating'); # orig=1 locl=x
847    declare_variable("$r.4.6",     'InMsgsRecipsOpenRelay');   # orig=0 locl=0
848    declare_variable("$r.4.7",     'InMsgsRecipsLocal');       # orig=x locl=1
849
850    declare_variable("$r.5.1",     'InMsgsBounce');
851    declare_variable("$r.5.2",     'InMsgsBounceNullRPath');
852    declare_variable("$r.5.3",     'InMsgsBounceKilled');
853    declare_variable("$r.5.4",     'InMsgsBounceUnverifiable');
854    declare_variable("$r.5.5",     'InMsgsBounceRescuedByDomain');
855    declare_variable("$r.5.6",     'InMsgsBounceRescuedByOriginating');
856    declare_variable("$r.5.7",     'InMsgsBounceRescuedByPenPals');
857
858    declare_variable("$r.6.1",     'OutMsgs');
859    declare_variable("$r.6.2",     'OutMsgsRelay');
860    declare_variable("$r.6.3",     'OutMsgsSubmit');
861    declare_variable("$r.6.4",     'OutMsgsSubmitQuar');
862    declare_variable("$r.6.5",     'OutMsgsSubmitDsn');
863    declare_variable("$r.6.6",     'OutMsgsSubmitNotif');
864    declare_variable("$r.6.7",     'OutMsgsSubmitAV');
865    declare_variable("$r.6.8",     'OutMsgsSubmitArf');
866    declare_variable("$r.6.9",     'OutMsgsProtoLocal');
867    declare_variable("$r.6.10",    'OutMsgsProtoLocalRelay');
868    declare_variable("$r.6.11",    'OutMsgsProtoLocalSubmit');
869    declare_variable("$r.6.12",    'OutMsgsProtoSMTP');
870    declare_variable("$r.6.13",    'OutMsgsProtoSMTPRelay');
871    declare_variable("$r.6.14",    'OutMsgsProtoSMTPSubmit');
872    declare_variable("$r.6.15",    'OutMsgsProtoLMTP');
873    declare_variable("$r.6.16",    'OutMsgsProtoLMTPRelay');
874    declare_variable("$r.6.17",    'OutMsgsProtoLMTPSubmit');
875    declare_variable("$r.6.18",    'OutMsgsProtoBSMTP');
876    declare_variable("$r.6.19",    'OutMsgsProtoBSMTPRelay');
877    declare_variable("$r.6.20",    'OutMsgsProtoBSMTPSubmit');
878    declare_variable("$r.6.21",    'OutMsgsProtoPipe');
879    declare_variable("$r.6.22",    'OutMsgsProtoPipeRelay');
880    declare_variable("$r.6.23",    'OutMsgsProtoPipeSubmit');
881    declare_variable("$r.6.24",    'OutMsgsProtoSQL');
882    declare_variable("$r.6.25",    'OutMsgsProtoSQLRelay');
883    declare_variable("$r.6.26",    'OutMsgsProtoSQLSubmit');
884    declare_variable("$r.6.27",    'OutMsgsDelivers');      # 2xx
885    declare_variable("$r.6.28",    'OutMsgsAttemptFails');  # 4xx
886    declare_variable("$r.6.29",    'OutMsgsRejects');       # 5xx
887
888    declare_variable("$r.7.1",     'OutMsgsSize',                'C64');
889    declare_variable("$r.7.2",     'OutMsgsSizeRelay',           'C64');
890    declare_variable("$r.7.3",     'OutMsgsSizeSubmit',          'C64');
891    declare_variable("$r.7.4",     'OutMsgsSizeSubmitQuar',      'C64');
892    declare_variable("$r.7.5",     'OutMsgsSizeSubmitDsn',       'C64');
893    declare_variable("$r.7.6",     'OutMsgsSizeSubmitNotif',     'C64');
894    declare_variable("$r.7.7",     'OutMsgsSizeSubmitAV',        'C64');
895    declare_variable("$r.7.8",     'OutMsgsSizeSubmitArf',       'C64');
896    declare_variable("$r.7.9",     'OutMsgsSizeProtoLocal',      'C64');
897    declare_variable("$r.7.10",    'OutMsgsSizeProtoLocalRelay', 'C64');
898    declare_variable("$r.7.11",    'OutMsgsSizeProtoLocalSubmit','C64');
899    declare_variable("$r.7.12",    'OutMsgsSizeProtoSMTP',       'C64');
900    declare_variable("$r.7.13",    'OutMsgsSizeProtoSMTPRelay',  'C64');
901    declare_variable("$r.7.14",    'OutMsgsSizeProtoSMTPSubmit', 'C64');
902    declare_variable("$r.7.15",    'OutMsgsSizeProtoLMTP',       'C64');
903    declare_variable("$r.7.16",    'OutMsgsSizeProtoLMTPRelay',  'C64');
904    declare_variable("$r.7.17",    'OutMsgsSizeProtoLMTPSubmit', 'C64');
905    declare_variable("$r.7.18",    'OutMsgsSizeProtoBSMTP',      'C64');
906    declare_variable("$r.7.19",    'OutMsgsSizeProtoBSMTPRelay', 'C64');
907    declare_variable("$r.7.20",    'OutMsgsSizeProtoBSMTPSubmit','C64');
908    declare_variable("$r.7.21",    'OutMsgsSizeProtoPipe',       'C64');
909    declare_variable("$r.7.22",    'OutMsgsSizeProtoPipeRelay',  'C64');
910    declare_variable("$r.7.23",    'OutMsgsSizeProtoPipeSubmit', 'C64');
911    declare_variable("$r.7.24",    'OutMsgsSizeProtoSQL',        'C64');
912    declare_variable("$r.7.25",    'OutMsgsSizeProtoSQLRelay',   'C64');
913    declare_variable("$r.7.26",    'OutMsgsSizeProtoSQLSubmit',  'C64');
914
915    declare_variable("$r.8.1",     'QuarMsgs');
916    declare_variable("$r.8.2",     'QuarMsgsArch');
917    declare_variable("$r.8.3",     'QuarMsgsClean');
918    declare_variable("$r.8.4",     'QuarMsgsMtaFailed');
919    declare_variable("$r.8.5",     'QuarMsgsOversized');
920    declare_variable("$r.8.6",     'QuarMsgsBadHdr');
921    declare_variable("$r.8.7",     'QuarMsgsSpammy');
922    declare_variable("$r.8.8",     'QuarMsgsSpam');
923    declare_variable("$r.8.9",     'QuarMsgsUnchecked');
924    declare_variable("$r.8.10",    'QuarMsgsBanned');
925    declare_variable("$r.8.11",    'QuarMsgsVirus');
926    declare_variable("$r.8.12",    'QuarAttemptTempFails');
927    declare_variable("$r.8.13",    'QuarAttemptFails');
928
929    declare_variable("$r.9.1",     'QuarMsgsSize',          'C64');
930    declare_variable("$r.9.2",     'QuarMsgsSizeArch',      'C64');
931    declare_variable("$r.9.3",     'QuarMsgsSizeClean',     'C64');
932    declare_variable("$r.9.4",     'QuarMsgsSizeMtaFailed', 'C64');
933    declare_variable("$r.9.5",     'QuarMsgsSizeOversized', 'C64');
934    declare_variable("$r.9.6",     'QuarMsgsSizeBadHdr',    'C64');
935    declare_variable("$r.9.7",     'QuarMsgsSizeSpammy',    'C64');
936    declare_variable("$r.9.8",     'QuarMsgsSizeSpam',      'C64');
937    declare_variable("$r.9.9",     'QuarMsgsSizeUnchecked', 'C64');
938    declare_variable("$r.9.10",    'QuarMsgsSizeBanned',    'C64');
939    declare_variable("$r.9.11",    'QuarMsgsSizeVirus',     'C64');
940
941    declare_variable("$r.10.1.1",  'ContentCleanMsgs');
942    declare_variable("$r.10.1.2",  'ContentCleanMsgsInbound');
943    declare_variable("$r.10.1.3",  'ContentCleanMsgsOutbound');
944    declare_variable("$r.10.1.4",  'ContentCleanMsgsInternal');
945    declare_variable("$r.10.1.5",  'ContentCleanMsgsOriginating');
946    declare_variable("$r.10.1.6",  'ContentCleanMsgsOpenRelay');
947
948    declare_variable("$r.10.2.1",  'ContentMtaFailedMsgs');
949    declare_variable("$r.10.2.2",  'ContentMtaFailedMsgsInbound');
950    declare_variable("$r.10.2.3",  'ContentMtaFailedMsgsOutbound');
951    declare_variable("$r.10.2.4",  'ContentMtaFailedMsgsInternal');
952    declare_variable("$r.10.2.5",  'ContentMtaFailedMsgsOriginating');
953    declare_variable("$r.10.2.6",  'ContentMtaFailedMsgsOpenRelay');
954
955    declare_variable("$r.10.3.1",  'ContentOversizedMsgs');
956    declare_variable("$r.10.3.2",  'ContentOversizedMsgsInbound');
957    declare_variable("$r.10.3.3",  'ContentOversizedMsgsOutbound');
958    declare_variable("$r.10.3.4",  'ContentOversizedMsgsInternal');
959    declare_variable("$r.10.3.5",  'ContentOversizedMsgsOriginating');
960    declare_variable("$r.10.3.6",  'ContentOversizedMsgsOpenRelay');
961
962    declare_variable("$r.10.4.1",  'ContentBadHdrMsgs');
963    declare_variable("$r.10.4.2",  'ContentBadHdrMsgsInbound');
964    declare_variable("$r.10.4.3",  'ContentBadHdrMsgsOutbound');
965    declare_variable("$r.10.4.4",  'ContentBadHdrMsgsInternal');
966    declare_variable("$r.10.4.5",  'ContentBadHdrMsgsOriginating');
967    declare_variable("$r.10.4.6",  'ContentBadHdrMsgsOpenRelay');
968
969    declare_variable("$r.10.5.1",  'ContentSpammyMsgs');
970    declare_variable("$r.10.5.2",  'ContentSpammyMsgsInbound');
971    declare_variable("$r.10.5.3",  'ContentSpammyMsgsOutbound');
972    declare_variable("$r.10.5.4",  'ContentSpammyMsgsInternal');
973    declare_variable("$r.10.5.5",  'ContentSpammyMsgsOriginating');
974    declare_variable("$r.10.5.6",  'ContentSpammyMsgsOpenRelay');
975
976    declare_variable("$r.10.6.1",  'ContentSpamMsgs');
977    declare_variable("$r.10.6.2",  'ContentSpamMsgsInbound');
978    declare_variable("$r.10.6.3",  'ContentSpamMsgsOutbound');
979    declare_variable("$r.10.6.4",  'ContentSpamMsgsInternal');
980    declare_variable("$r.10.6.5",  'ContentSpamMsgsOriginating');
981    declare_variable("$r.10.6.6",  'ContentSpamMsgsOpenRelay');
982
983    declare_variable("$r.10.7.1",  'ContentUncheckedMsgs');
984    declare_variable("$r.10.7.2",  'ContentUncheckedMsgsInbound');
985    declare_variable("$r.10.7.3",  'ContentUncheckedMsgsOutbound');
986    declare_variable("$r.10.7.4",  'ContentUncheckedMsgsInternal');
987    declare_variable("$r.10.7.5",  'ContentUncheckedMsgsOriginating');
988    declare_variable("$r.10.7.6",  'ContentUncheckedMsgsOpenRelay');
989
990    declare_variable("$r.10.8.1",  'ContentBannedMsgs');
991    declare_variable("$r.10.8.2",  'ContentBannedMsgsInbound');
992    declare_variable("$r.10.8.3",  'ContentBannedMsgsOutbound');
993    declare_variable("$r.10.8.4",  'ContentBannedMsgsInternal');
994    declare_variable("$r.10.8.5",  'ContentBannedMsgsOriginating');
995    declare_variable("$r.10.8.6",  'ContentBannedMsgsOpenRelay');
996
997    declare_variable("$r.10.9.1",  'ContentVirusMsgs');
998    declare_variable("$r.10.9.2",  'ContentVirusMsgsInbound');
999    declare_variable("$r.10.9.3",  'ContentVirusMsgsOutbound');
1000    declare_variable("$r.10.9.4",  'ContentVirusMsgsInternal');
1001    declare_variable("$r.10.9.5",  'ContentVirusMsgsOriginating');
1002    declare_variable("$r.10.9.6",  'ContentVirusMsgsOpenRelay');
1003
1004    declare_variable("$r.11.1",    'CacheAttempts');
1005    declare_variable("$r.11.2",    'CacheMisses');
1006    declare_variable("$r.11.3",    'CacheHits');
1007    declare_variable("$r.11.4",    'CacheHitsVirusCheck');
1008    declare_variable("$r.11.5",    'CacheHitsVirusMsgs');
1009    declare_variable("$r.11.6",    'OutConnNew');
1010    declare_variable("$r.11.7",    'OutConnQuit');
1011    declare_variable("$r.11.8",    'OutConnTransact');
1012    declare_variable("$r.11.9",    'OutConnReuseFail');
1013    declare_variable("$r.11.10",   'OutConnReuseRecent');
1014    declare_variable("$r.11.11",   'OutConnReuseRefreshed');
1015
1016    declare_variable("$r.12.1",    'OpsDec');
1017    declare_variable("$r.12.2",    'OpsSpamCheck');
1018    declare_variable("$r.12.3",    'OpsVirusCheck');
1019
1020    declare_variable("$r.13.1",    'PenPalsAttempts');
1021    declare_variable("$r.13.2",    'PenPalsAttemptsRid');
1022    declare_variable("$r.13.3",    'PenPalsAttemptsMid');
1023    declare_variable("$r.13.4",    'PenPalsMisses');
1024    declare_variable("$r.13.5",    'PenPalsHits');
1025    declare_variable("$r.13.6",    'PenPalsHitsRid');
1026    declare_variable("$r.13.7",    'PenPalsHitsMid');
1027    declare_variable("$r.13.8",    'PenPalsHitsMidRid');
1028    declare_variable("$r.13.9",    'PenPalsSavedFromTag2');
1029    declare_variable("$r.13.10",   'PenPalsSavedFromTag3');
1030    declare_variable("$r.13.11",   'PenPalsSavedFromKill');
1031
1032    declare_variable("$r.14.1",    'SqlAddrSenderAttempts');
1033    declare_variable("$r.14.2",    'SqlAddrSenderMisses');
1034    declare_variable("$r.14.3",    'SqlAddrSenderHits');
1035    declare_variable("$r.14.4",    'SqlAddrRecipAttempts');
1036    declare_variable("$r.14.5",    'SqlAddrRecipMisses');
1037    declare_variable("$r.14.6",    'SqlAddrRecipHits');
1038
1039    declare_variable("$r.15.1",    'LogEntries',           'C64');
1040    declare_variable("$r.15.2",    'LogEntriesEmerg',      'C64');
1041    declare_variable("$r.15.3",    'LogEntriesAlert',      'C64');
1042    declare_variable("$r.15.4",    'LogEntriesCrit',       'C64'); # lvl le -3
1043    declare_variable("$r.15.5",    'LogEntriesErr',        'C64'); # lvl le -2
1044    declare_variable("$r.15.6",    'LogEntriesWarning',    'C64'); # lvl le -1
1045    declare_variable("$r.15.7",    'LogEntriesNotice',     'C64'); # lvl le  0
1046    declare_variable("$r.15.8",    'LogEntriesInfo',       'C64'); # lvl le  1
1047    declare_variable("$r.15.9",    'LogEntriesDebug',      'C64'); # lvl le  2
1048    declare_variable("$r.15.10",   'LogEntriesLevel0',     'C64'); # le 0
1049    declare_variable("$r.15.11",   'LogEntriesLevel1',     'C64'); # eq 1
1050    declare_variable("$r.15.12",   'LogEntriesLevel2',     'C64'); # eq 2
1051    declare_variable("$r.15.13",   'LogEntriesLevel3',     'C64'); # eq 3
1052    declare_variable("$r.15.14",   'LogEntriesLevel4',     'C64'); # eq 4
1053    declare_variable("$r.15.15",   'LogEntriesLevel5',     'C64'); # ge 5
1054    declare_variable("$r.15.16",   'LogLines',             'C64');
1055    declare_variable("$r.15.17",   'LogRetries',           'C64');
1056
1057    declare_variable("$r.16.1",    'TimeElapsedTotal',     'INT');
1058    declare_variable("$r.16.2",    'TimeElapsedReceiving', 'INT');
1059    declare_variable("$r.16.3",    'TimeElapsedSending',   'INT');
1060    declare_variable("$r.16.4",    'TimeElapsedDecoding',  'INT');
1061    declare_variable("$r.16.5",    'TimeElapsedPenPals',   'INT');
1062    declare_variable("$r.16.6",    'TimeElapsedVirusCheck','INT');
1063    declare_variable("$r.16.7",    'TimeElapsedSpamCheck', 'INT');
1064
1065    declare_variable("$r.17.1",    'UserCounter1',         'C64');
1066    declare_variable("$r.17.2",    'UserCounter2',         'C64');
1067    declare_variable("$r.17.3",    'UserCounter3',         'C64');
1068    declare_variable("$r.17.4",    'UserCounter4',         'C64');
1069    declare_variable("$r.17.5",    'UserCounter5',         'C64');
1070    declare_variable("$r.17.6",    'UserCounter6',         'C64');
1071    declare_variable("$r.17.7",    'UserCounter7',         'C64');
1072    declare_variable("$r.17.8",    'UserCounter8',         'C64');
1073    declare_variable("$r.17.9",    'UserCounter9',         'C64');
1074    declare_variable("$r.17.10",   'UserCounter10',        'C64');
1075
1076    declare_variable("$r.18.1",    'UserGauge1',           'G32');
1077    declare_variable("$r.18.2",    'UserGauge2',           'G32');
1078    declare_variable("$r.18.3",    'UserGauge3',           'G32');
1079    declare_variable("$r.18.4",    'UserGauge4',           'G32');
1080    declare_variable("$r.18.5",    'UserGauge5',           'G32');
1081    declare_variable("$r.18.6",    'UserGauge6',           'G32');
1082    declare_variable("$r.18.7",    'UserGauge7',           'G32');
1083    declare_variable("$r.18.8",    'UserGauge8',           'G32');
1084    declare_variable("$r.18.9",    'UserGauge9',           'G32');
1085    declare_variable("$r.18.10",   'UserGauge10',          'G32');
1086
1087    declare_variable("$r.19.1",    'InMsgsStatusAccepted');    # 2xx, AM.PDP
1088    declare_variable("$r.19.2",    'InMsgsStatusAcceptedInbound');
1089    declare_variable("$r.19.3",    'InMsgsStatusAcceptedOutbound');
1090    declare_variable("$r.19.4",    'InMsgsStatusAcceptedInternal');
1091    declare_variable("$r.19.5",    'InMsgsStatusAcceptedOriginating');
1092    declare_variable("$r.19.6",    'InMsgsStatusAcceptedOpenRelay');
1093
1094    declare_variable("$r.20.1",    'InMsgsStatusRelayedUntagged');  # 2xx, fwd
1095    declare_variable("$r.20.2",    'InMsgsStatusRelayedUntaggedInbound');
1096    declare_variable("$r.20.3",    'InMsgsStatusRelayedUntaggedOutbound');
1097    declare_variable("$r.20.4",    'InMsgsStatusRelayedUntaggedInternal');
1098    declare_variable("$r.20.5",    'InMsgsStatusRelayedUntaggedOriginating');
1099    declare_variable("$r.20.6",    'InMsgsStatusRelayedUntaggedOpenRelay');
1100
1101    declare_variable("$r.21.1",    'InMsgsStatusRelayedTagged'); # 2xx, forward
1102    declare_variable("$r.21.2",    'InMsgsStatusRelayedTaggedInbound');
1103    declare_variable("$r.21.3",    'InMsgsStatusRelayedTaggedOutbound');
1104    declare_variable("$r.21.4",    'InMsgsStatusRelayedTaggedInternal');
1105    declare_variable("$r.21.5",    'InMsgsStatusRelayedTaggedOriginating');
1106    declare_variable("$r.21.6",    'InMsgsStatusRelayedTaggedOpenRelay');
1107
1108    declare_variable("$r.22.1",    'InMsgsStatusDiscarded');   # 2xx, no DSN
1109    declare_variable("$r.22.2",    'InMsgsStatusDiscardedInbound');
1110    declare_variable("$r.22.3",    'InMsgsStatusDiscardedOutbound');
1111    declare_variable("$r.22.4",    'InMsgsStatusDiscardedInternal');
1112    declare_variable("$r.22.5",    'InMsgsStatusDiscardedOriginating');
1113    declare_variable("$r.22.6",    'InMsgsStatusDiscardedOpenRelay');
1114
1115    declare_variable("$r.23.1",    'InMsgsStatusNoBounce');    # 2xx, no DSN
1116    declare_variable("$r.23.2",    'InMsgsStatusNoBounceInbound');
1117    declare_variable("$r.23.3",    'InMsgsStatusNoBounceOutbound');
1118    declare_variable("$r.23.4",    'InMsgsStatusNoBounceInternal');
1119    declare_variable("$r.23.5",    'InMsgsStatusNoBounceOriginating');
1120    declare_variable("$r.23.6",    'InMsgsStatusNoBounceOpenRelay');
1121
1122    declare_variable("$r.24.1",    'InMsgsStatusBounced');     # 2xx, DSN sent
1123    declare_variable("$r.24.2",    'InMsgsStatusBouncedInbound');
1124    declare_variable("$r.24.3",    'InMsgsStatusBouncedOutbound');
1125    declare_variable("$r.24.4",    'InMsgsStatusBouncedInternal');
1126    declare_variable("$r.24.5",    'InMsgsStatusBouncedOriginating');
1127    declare_variable("$r.24.6",    'InMsgsStatusBouncedOpenRelay');
1128
1129    declare_variable("$r.25.1",    'InMsgsStatusRejected');    # 5xx
1130    declare_variable("$r.25.2",    'InMsgsStatusRejectedInbound');
1131    declare_variable("$r.25.3",    'InMsgsStatusRejectedOutbound');
1132    declare_variable("$r.25.4",    'InMsgsStatusRejectedInternal');
1133    declare_variable("$r.25.5",    'InMsgsStatusRejectedOriginating');
1134    declare_variable("$r.25.6",    'InMsgsStatusRejectedOpenRelay');
1135
1136    declare_variable("$r.26.1",    'InMsgsStatusTempFailed');  # 4xx
1137    declare_variable("$r.26.2",    'InMsgsStatusTempFailedInbound');
1138    declare_variable("$r.26.3",    'InMsgsStatusTempFailedOutbound');
1139    declare_variable("$r.26.4",    'InMsgsStatusTempFailedInternal');
1140    declare_variable("$r.26.5",    'InMsgsStatusTempFailedOriginating');
1141    declare_variable("$r.26.6",    'InMsgsStatusTempFailedOpenRelay');
1142  }
1143
1144  { # amavisd child processes MIB
1145    my($r) = $databases[1]->{root_oid_str};
1146    declare_variable("$r.1.1",     'ProcGone');                    # counter!
1147    declare_variable("$r.1.2",     'ProcAll',              'G32');
1148    declare_variable("$r.1.3",     'ProcIdle',             'G32');
1149    declare_variable("$r.1.4",     'ProcBusy',             'G32');
1150    declare_variable("$r.1.5",     'ProcBusyTransfer',     'G32');
1151    declare_variable("$r.1.6",     'ProcBusyDecode',       'G32');
1152    declare_variable("$r.1.7",     'ProcBusyVirus',        'G32');
1153    declare_variable("$r.1.8",     'ProcBusySpam',         'G32');
1154    declare_variable("$r.1.9",     'ProcBusyOther',        'G32');
1155    declare_variable(sprintf("%s.2.%d", $r,$_+1),
1156                                   'ProcBusy'.$_, 'G32')  for (0..@age_slots);
1157  }
1158
1159  if (defined $mta_queue_dir) {
1160    # Postfix queue size MIB
1161    declare_variable($databases[2]->{root_oid_str},
1162                                   'MtaQueueEntriesMaildrop', 'G32');
1163    declare_variable($databases[3]->{root_oid_str},
1164                                   'MtaQueueEntriesIncoming', 'G32');
1165    declare_variable($databases[4]->{root_oid_str},
1166                                   'MtaQueueEntriesActive',   'G32');
1167    declare_variable($databases[5]->{root_oid_str},
1168                                   'MtaQueueEntriesDeferred', 'G32');
1169  }
1170
1171  if (!$daemonize) {
1172    do_log(0,"%s starting in foreground, perl %s", $myversion,$]);
1173  } else {  # daemonize
1174    $SIG{'__WARN__'} =  # log warnings
1175      sub { my($m) = @_; chomp($m); do_log(-1,"_WARN: %s",$m) };
1176    $SIG{'__DIE__' } =  # log uncaught errors
1177      sub { if (!$^S) { my($m) = @_; chomp($m); do_log(-2,"_DIE: %s",$m) } };
1178    openlog($syslog_ident, LOG_PID | LOG_NDELAY, $syslog_facility);
1179    $syslog_open = 1;
1180    do_log(2,"to be daemonized");
1181    daemonize();
1182    do_log(0,"%s starting. daemonized as PID [%s], perl %s", $myversion,$$,$]);
1183    if (defined $pid_filename && $pid_filename ne '') {
1184      my($pidf) = IO::File->new;
1185      my($stat) = $pidf->open($pid_filename, O_CREAT|O_EXCL|O_RDWR, 0640);
1186      if (!$stat && $! == EEXIST) {
1187        do_log(0,"PID file %s exists, overwriting", $pid_filename);
1188        $stat = $pidf->open($pid_filename, O_CREAT|O_RDWR, 0640);
1189      }
1190      $stat or die "Can't create file $pid_filename: $!";
1191      $pid_file_created = 1;
1192      $pidf->print("$$\n") or die "Can't write to $pid_filename: $!";
1193      $pidf->close or die "Can't close $pid_filename: $!";
1194    }
1195  }
1196
1197  #netsnmp_ds_set_boolean(NETSNMP_DS_APPLICATION_ID,
1198  #                       NETSNMP_DS_LIB_DONT_READ_CONFIGS, 1);
1199
1200  my($agent) = NetSNMP::agent->new('Name' => $agent_name, 'AgentX' => 1)
1201    or die "Can't create a SNMP agent $agent_name";
1202
1203  init_data();  # must come *after* NetSNMP::agent->new
1204# dump_variables();
1205
1206  for my $database (@databases) {
1207    my($root_oid_str) = $database->{root_oid_str};
1208    my($db_name) = $database->{name};
1209    if ($db_name =~ /^pf/ && !defined $mta_queue_dir) {
1210      do_log(2, "not registering root OID %s for %s", $root_oid_str,$db_name);
1211    } else {
1212      do_log(2, "registering root OID %s for %s", $root_oid_str,$db_name);
1213      $root_oid_str = '.' . $root_oid_str;
1214      $agent->register($agent_name, $root_oid_str, \&snmp_handler)
1215        or die "Can't register a SNMP agent $agent_name under $root_oid_str";
1216      $database->{registered} = 1;
1217    }
1218  }
1219
1220  while ($keep_running) {
1221    $agent->agent_check_and_process(1);
1222  }
1223  exit;
1224
1225END {
1226  if (defined $agent) {
1227    eval { $agent->shutdown };  # ignoring status
1228  }
1229  for my $database (@databases) {
1230    eval {
1231      if (defined $database->{db}) {
1232        if (defined $database->{cursor}) {
1233          $database->{cursor}->c_close;  # close database, ignoring status
1234          undef $database->{cursor};
1235        }
1236        $database->{db}->db_close == 0
1237          or warn(sprintf("BDB db_close error on a %s file: %s %s",
1238                          $database->{db}, $BerkeleyDB::Error, $!));
1239      }
1240    };  # ignoring status
1241  }
1242  if ($pid_file_created) {
1243    unlink($pid_filename)
1244      or eval { do_log(0, "Can't remove file %s: %s", $pid_filename,$!) };
1245  }
1246  eval { do_log(2, "%s shutting down", $myproduct_name) };
1247  if ($syslog_open) {
1248    eval { closelog() }; $syslog_open = 0;
1249  }
1250}
1251