#!/bin/sh #! -*-perl-*- # This file is part of NetSNMP::Sendmail # Copyright (C) 2019-2020 Sergey Poznyakoff # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . eval 'exec perl -x -wS $0 ${1+"$@"}' if 0; use strict; use warnings; use File::Temp; use File::Basename; use IPC::Cmd qw(can_run); use POSIX qw(strftime); use Fcntl; use Getopt::Long qw(:config gnu_getopt no_ignore_case require_order); use Pod::Usage; sub addts { my $fd = shift; print $fd "# Line added by $0 at " . strftime("%Y-%m-%dT%H:%M:%S", localtime) . "\n"; } my $my_ts_rx = qr{^# Line added by $0 at }; use constant { EX_OK => 0, # Success EX_UNCHANGED => 1, # Files not changed EX_FATAL => 2, # Fatal error EX_USAGE => 64 # Usage error }; use constant { CMD_SETUP => 0, CMD_REMOVE => 1 }; my $suppress_level = 0; my $dry_run; my @updated_services; use constant { L_INFO => 0, L_NOTICE => 1, L_WARN => 2, L_ERR => 3 }; use constant MAX_SUPPRESS_LEVEL => L_WARN; sub printlog { my $level = shift; if ($suppress_level <= $level) { my $fh = (($level >= L_WARN) ? \*STDERR : \*STDOUT); print $fh "$0: ".join(' ',@_)."\n"; } } my @restart_commands = ( [qw(systemctl restart $service)], [qw(service $service restart)], [qw(/etc/init.d/$service restart)], [qw(/etc/rc.d/$service restart)], ); my %restart_override; sub restart_service { my $service = shift; my $cmd = $restart_override{$service}; return if $cmd && $cmd eq 'no'; printlog(L_NOTICE, "restarting $service"); return if $dry_run; if ($cmd) { printlog(L_NOTICE, "running $cmd"); system($cmd); } else { foreach $cmd (@restart_commands) { my @c = map { (my $s = $_) =~ s/\$service/$service/g; $s } @$cmd; if (can_run($c[0])) { printlog(L_NOTICE, "running @c"); system(@c); return if ($? == 0); } } } } sub file_replace { my ($file, $newfile) = @_; my $bk = "$file~"; unlink $bk if -e $bk; rename $file, $bk or die "can't rename $file to $bk: $!"; unless (rename $newfile, $file) { printlog(L_WARN, "can't rename $newfile to $file: $!; restoring from backup"); unless (rename $bk, $file) { printlog(L_ERR, "failed to rename $bk to $file: $!"); exit(EX_FATAL); } } } sub file_remline { my ($file, $fd, $line, $endline) = @_; $endline //= $line; printlog(L_NOTICE, ($endline == $line) ? "editing $file: removing line $line" : "editing $file: removing lines $line-$endline"); return if $dry_run; my $ofd = File::Temp->new(DIR => dirname($file), UNLINK => $dry_run); seek($fd, 0, SEEK_SET) or die "seek $file: $!"; my $ln = 0; while (<$fd>) { $ln++; next if ($line <= $ln && $ln <= $endline); print $ofd $_; } close $ofd; close $fd; file_replace($file, $ofd->filename); } sub scan_snmpd_conf { my ($file, $fd) = @_; my $line = 0; my $comline; my $insert_line; while (<$fd>) { ++$line; chomp; s/^\s+//; if (/$my_ts_rx/) { $comline = $line; next; } if (/^perl\s+use\s+NetSNMP::Sendmail/) { if ($comline && $comline + 1 == $line) { return (1, $comline, $line); } else { return (1, $line); } } if (/^perl\s+use/) { $insert_line = $line; } } return (0, $insert_line || $line + 1); } sub update_snmpd_conf { my ($ifile, $ifd, $insert_line, $stmt) = @_; seek($ifd, 0, SEEK_SET) or die "seek: $!"; my $ofd = File::Temp->new(DIR => dirname($ifile), UNLINK => $dry_run); my $line = 0; while (<$ifd>) { chomp; ++$line; if ($insert_line && $line == $insert_line) { printlog(L_NOTICE, "editing $ifile (line $line)"); addts $ofd; print $ofd "$stmt\n"; $insert_line = undef; } print $ofd "$_\n"; } if ($insert_line) { printlog(L_NOTICE, "editing $ifile (append)"); addts $ofd; print $ofd "$stmt\n"; } close $ofd; file_replace($ifile, $ofd->filename) unless ($dry_run); } sub edit_snmpd_conf { my ($command, $name, $stmt) = @_; my $u = umask(077); if (open(my $fd, '<', $name)) { my ($found, $line, $endline) = scan_snmpd_conf($name, $fd); if ($command == CMD_SETUP) { if ($found) { printlog(L_INFO, "$name:".($endline ? $endline : $line).": NetSNMP::Sendmail already enabled"); } else { update_snmpd_conf($name, $fd, $line, $stmt); push @updated_services, 'snmpd'; } } elsif ($found) { file_remline($name, $fd, $line, $endline); push @updated_services, 'snmpd'; } close $fd; } else { printlog(L_ERR, "can't open $name: $!"); exit(EX_FATAL); } umask($u); } sub check_file { my $file = shift; if (-f $file) { if (! -r $file) { printlog(L_ERR, "$file is not readable"); exit(EX_FATAL); } } else { printlog(L_ERR, "$file does not exist"); exit(EX_FATAL); } } # Check if NetSNMP::Sendmail is available. # To avoid namespace contamination, do it in a subprocess. # The module requires SmtpAgent.pm, whic spits out lots of messages # to STDERR when loaded outside of snmpd, so first redirect stderr # to /dev/null. sub check_module { my $pid = fork(); die "fork failed" unless defined $pid; if ($pid == 0) { open(STDERR, '>', '/dev/null'); require NetSNMP::Sendmail; exit 0; } wait; if ($?) { printlog(L_ERR, "NetSNMP::Sendmail doesn't seem to be installed"); exit(EX_FATAL); } } sub scan_sendmail_mc { my $fd = shift; my $name; my $last_nl; my $comline; my $line = 0; while (<$fd>) { $line++; $last_nl = chomp; s/^\s+//; if (/$my_ts_rx/) { $comline = $line; next; } if (/^define\(\s*`?STATUS_FILE'?\s*,\s*`?(.+?)'?\s*\)/) { $name = $1; last } } return ($name, $last_nl, ($comline && $comline + 1 == $line) ? ($comline, $line) : ($line)); } sub edit_sendmail_mc { my ($command, $file, $default_statfile) = @_; if (open(my $fd, '+<', $file)) { my $need_make; my ($statfile, $last_nl, $line, $endline) = scan_sendmail_mc($fd); if ($command == CMD_SETUP) { if ($statfile) { printlog(L_INFO, "$file:".($endline ? $endline : $line). ": status file $statfile already enabled"); } else { $statfile = $default_statfile; printlog(L_NOTICE, "editing $file"); unless ($dry_run) { seek($fd, 0, SEEK_END) or die "seek: $!"; print $fd "\n" unless $last_nl; addts $fd; print $fd "define(`STATUS_FILE', `$statfile')\n"; } push @updated_services, 'sendmail'; $need_make = !$dry_run; } if (-f $statfile) { printlog(L_INFO, "$statfile exists"); } else { printlog(L_NOTICE, "creating $statfile"); unless ($dry_run) { if (open($fd, '>', $statfile)) { close($fd); } else { warn "failed to create $statfile: $!"; } } } } elsif ($statfile) { if ($endline) { file_remline($file, $fd, $line, $endline); push @updated_services, 'sendmail'; $need_make = !$dry_run; } else { printlog(L_INFO, "$file:$line: retaining status file setup: not configured by $0"); } } close $fd; if ($need_make) { my $sendmail_dir = dirname($file); printlog(L_NOTICE, "running make in $sendmail_dir"); system("make " . ($dry_run ? '-n ' : '') . "-C $sendmail_dir"); } } else { printlog(L_ERR, "can't open $file: $!"); exit(EX_FATAL); } } # Main my $snmpd_conf = '/etc/snmp/snmpd.conf'; my $sendmail_mc = '/etc/mail/sendmail.mc'; my $sendmail_statfile = '/etc/mail/sendmail.st'; my $sendmail_bindir; my $command = CMD_SETUP; GetOptions('quiet|q+' => \$suppress_level, 'dry-run|n' => \$dry_run, 'status-file=s' => \$sendmail_statfile, 'bindir=s' => \$sendmail_bindir, 'restart=s' => sub { my ($name,$cmd) = split /=/, $_[1], 2; $restart_override{$name} = $cmd; }, 'configure' => sub { $command = CMD_SETUP }, 'deconfigure' => sub { $command = CMD_REMOVE }, 'help' => sub { pod2usage(-exitstatus => EX_OK, -verbose => 2); }, 'usage' => sub { pod2usage(-exitstatus => EX_OK, -verbose => 0); } ) or pod2usage(-exitstatus => EX_USAGE, -verbose => 0, -output => \*STDERR); pod2usage(-exitstatus => EX_USAGE, -verbose => 0, -output => \*STDERR) if @ARGV; if ($suppress_level > MAX_SUPPRESS_LEVEL) { $suppress_level = MAX_SUPPRESS_LEVEL; } check_module; check_file($snmpd_conf); check_file($sendmail_mc); check_file('/etc/mail/Makefile'); unless ($sendmail_bindir) { if (-d "/usr/lib/sm.bin") { $sendmail_bindir = "/usr/lib/sm.bin"; } } my $stmt = 'perl use NetSNMP::Sendmail'; if ($sendmail_bindir) { $stmt .= ' qw(:config bindir /usr/lib/sm.bin)'; } $stmt .= ';'; edit_sendmail_mc($command, $sendmail_mc, $sendmail_statfile); edit_snmpd_conf($command, $snmpd_conf, $stmt); map { restart_service($_) } @updated_services; exit(@updated_services ? EX_OK : EX_UNCHANGED); __END__ =head1 NAME netsnmp-sendmail-setup - sets up Sendmail monitoring via SNMP =head1 SYNOPSIS B [B<-nq>] [B<--bindir=I>] [B<--configure>] [B<--deconfigure>] [B<--dry-run>] [B<--status-file=I>] [B<--quiet>] [B<--restart=I=I>] B B<--help> | B<--usage> =head1 DESCRIPTION Sets up B and B for obtaining Sendmail statistics via SNMP. First, it checks whether the Sendmail configuration source F contains the B clause and adds it if not. Then, it creates the status file and runs B in the F directory. Finally, the file F is scanned for the B statement. It is added if not already present. Each added configuration line is preceded by a comment stating that it was added by the script. When run with the B<--deconfigure> option, the reverse operation is performed. The B configuration statement is removed from the snmpd configuration unconditionally. The B clause is removed from F only if it is preceded by the B comment marker. The status file itself is never removed. =head1 OPTIONS =over 4 =item B<--bindir=I> Some installations place Sendmail binaries in a separate directory, which is not included in the B<$PATH> environment variable. Use this option to inform B about this. Notice for the users of Debian-based systems: the F directory is picked up automatically. =item B<--configure> A no-op option included for symmetry with B<--deconfigure>. =item B<--deconfigure> Remove the configuration statements previously added to the snmdp and sendmail configuration files. =item B<-n>, B<--dry-run> Dry run mode. Don't modify any files, just print what would have been done and exit with the appropriate error code (see B section). Use the B<--quiet> option to control the amount of data printed. =item B<-q>, B<--quiet> Quiet mode. When used once, suppresses informative output. When used twice, suppresses both informative output and notification messages about modified files. =item B<--restart=I=I> Use I to restart system service I (either B or B). Use B<--restart=I=no> to skip restarting this particular I. In the absence of this option, B uses the first available command from the following list: systemctl restart $service service $service restart /etc/init.d/$service restart /etc/rc.d/$service restart =item B<--status-file=I> Name of the Sendmail status file to use when generating the B statement in the Sendmail configuration file. =item B<--help> Displays short help message. =item B<--usage> Displaye short usage message. =back =head1 FILES =over 4 =item F This file is supposed to recreate the B file from B if no special goal is given. =item F Default B configuration file. This file must exist. =item F Default source file for creating B. =item F Default statistics file to use. Can be changed using the B<--status-file> option. =item F Default directory for Sendmail binaries on Debian-based installations. If exists, it will be used in the B configuration. This can be changed using the B<--bindir> command line option. =back =head1 EXIT STATUS =over 4 =item B<0> Success. =item B<1> Success, no modification was necessary. =item B<2> Fatal error occurred. =item B<64> Command line usage error. =back =head1 SEE ALSO B(3). =head1 BUGS The Sendmail configuration directory and the name of the B configuration file are hardcoded. The command relies on B to create F from F. =cut