#!/usr/bin/perl # # proftpdlogrotate.pl - rotate ProFTPd logs cleanly # Copyright (C) 2002 - Anthony Tonns # # 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 2 # of the License, 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, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # --- # # ATonns Sun Jul 14 15:23:28 EDT 2002 # gutted apachelogrotate.pl to create this script # ##### admin configuration ##### use strict; use File::Basename; my $DEBUG = 0; # where is the config file? my $configfile="/usr/local/etc/proftpdlogrotate.cfg"; # how long do we want to wait between # between tries for checking for zero filehandles my $sleeptime=120; # maximum tries for zero filehandles my $maxtries=30; # who do we want to bug? my @victims_email = ( "devnull\@tonns.com" ); my @victims_pagers = ( "devnull\@tonns.com" ); # date format # old - mmddyy # new - yyyymmdd # defaults to "new" if not specified my $dateformat = "new"; ##### end admin configuration ##### # stuff that shouldn't change, but should not be hardcoded my $progname="proftpdlogrotate.pl"; # where are we running? my $hostname = `uname -n`; chomp $hostname; # static program locations my $GZIP="/usr/bin/gzip"; my $CAT="/usr/bin/cat"; my $MV="/usr/bin/mv"; my $GREP="/usr/bin/grep"; my $MAIL="/usr/bin/mail"; my $DF="/usr/bin/df"; my $LSOF="/usr/local/bin/lsof"; # what extention do our compressed files have? my $gzip_extention="gz"; # get the date my @dateinfo = localtime(time); my $longyear = $dateinfo[5] + 1900; my $shortyear = $dateinfo[5]; $shortyear = "0$shortyear" if $shortyear < 10; my $month = $dateinfo[4] + 1; $month = "0$month" if $month < 10; my $day = $dateinfo[3]; $day = "0$day" if $day < 10; # set the default format, or use the old one my $filedate = "$longyear$month$day"; if ( $dateformat eq "old" ) { $filedate = "$month$day$shortyear"; } # for email subject status (hope for the best) my $status="success"; # some data structures to do the job # (and record it) my @sitecfg = (); my @realsites = (); my @reportlines = (); # # step 0: wait for midnight # sleep(60) if ! $DEBUG; # # step 1: read the config file # my $linenum = 0; open(CFG,"<$configfile") || &kick_the_bucket("cannot open config file $configfile"); while() { $linenum++; # ignore comments and blank lines next if ( /^$/ || /^#/ ); # setup the config my($rawlogfile,$logdest,$minfree) = split(/\s+/); my $sitenum = $#sitecfg + 1; $sitecfg[$sitenum]->{"rawlogfile"} = $rawlogfile; $sitecfg[$sitenum]->{"logdest"} = $logdest; $sitecfg[$sitenum]->{"minfree"} = $minfree; } close(CFG); # # step 2: sanity checking on user config 'n stuff # # fatal exceptions ( -f $GZIP ) || &kick_the_bucket("gzip($GZIP) not found"); ( -f $CAT ) || &kick_the_bucket("cat($CAT) not found"); ( -f $MV ) || &kick_the_bucket("mv($MV) not found"); ( -f $GREP ) || &kick_the_bucket("grep($GREP) not found"); ( -f $MAIL ) || &kick_the_bucket("mail($MAIL) not found"); ( -f $DF ) || &kick_the_bucket("df($DF) not found"); ( -f $LSOF ) || &kick_the_bucket("lsof($LSOF) not found"); # not so fatal exceptions for (my $sitecfgnum=0; $sitecfgnum <= $#sitecfg; $sitecfgnum++) { my $rawlogfile = $sitecfg[$sitecfgnum]->{"rawlogfile"}; if ( ! -f $rawlogfile ) { &admonish("logfile: $rawlogfile not found, skipping"); next; } my $logdest = $sitecfg[$sitecfgnum]->{"logdest"}; if ( ! -d $logdest ) { &admonish("logdest dir: $logdest not found, skipping"); next; } my $realsitenum = $#realsites + 1; $realsites[$realsitenum]->{"rawlogfile"} = $sitecfg[$sitecfgnum]->{"rawlogfile"}; $realsites[$realsitenum]->{"logdest"} = $sitecfg[$sitecfgnum]->{"logdest"}; $realsites[$realsitenum]->{"minfree"} = $sitecfg[$sitecfgnum]->{"minfree"}; &report("adding $rawlogfile -> $logdest to archival operation"); } # # step 3: do the rotation # # first pass, just get the log files out of the way for (my $realsitenum=0; $realsitenum <= $#realsites; $realsitenum++) { my $rawlogfile = $realsites[$realsitenum]->{"rawlogfile"}; my $logdest = $realsites[$realsitenum]->{"logdest"}; my $minfree = $realsites[$realsitenum]->{"minfree"}; my $error = 0; $error = &move_logs($rawlogfile); if ($error) { &admonish("problems moving $rawlogfile, " . "archival operation in ugly state: $error"); $error=0; } else { &report("successfully moved $rawlogfile"); } } sleep($sleeptime) if ! $DEBUG; # second pass, gzip the log files to "somewhere else" for (my $realsitenum=0; $realsitenum <= $#realsites; $realsitenum++) { my $rawlogfile = $realsites[$realsitenum]->{"rawlogfile"}; my $logdest = $realsites[$realsitenum]->{"logdest"}; my $minfree = $realsites[$realsitenum]->{"minfree"}; my $error = 0; $error = &checkfree($logdest,$minfree); if ($error) { &scream("not enough free space on $logdest, " . "skipping gzip: $error"); $error=0; next; } $error = &checkfilehandles($rawlogfile); if ($error) { &scream("logs still open for $rawlogfile, " . "skipping gzip: $error"); $error=0; next; } $error = &gzip_logs($rawlogfile,$logdest); if ($error) { &scream("couldn't gzip $rawlogfile, " . "archival operation hosed: $error"); $error=0; } else { &report("successfully gzip'ed $rawlogfile"); } } # # step 4: build report and email it # unshift @reportlines, "-" x 80 . "\n"; unshift @reportlines, "apache log rotation report for $filedate\n"; unshift @reportlines, "-" x 80 . "\n"; push @reportlines, "-" x 80 . "\n"; my $victim; foreach $victim (@victims_email) { open(REPORT, "| $MAIL -s $progname-$hostname-$status $victim"); foreach (@reportlines) { print REPORT $_; } close(REPORT); } ### support subroutines ### sub move_logs { my $rawlogfile = shift @_; my $log = basename($rawlogfile); my $dirname = dirname($rawlogfile); my $abs_file_src = "$dirname/$log"; my $abs_file_dst = "$dirname/$filedate.$log"; system("$MV $abs_file_src $abs_file_dst"); if ($?) { &admonish("mv failed for $abs_file_src " . "to $abs_file_dst. exit: $?"); return("mv failed"); } return 0; } sub checkfree { # check freespace my ($logdest,$minfree) = @_; my ($free,$dfstring); chomp ( $dfstring = `$DF -k $logdest | $GREP dev` ); #Filesystem 1024-blocks Used Available Capacity Mounted on #/dev/dsk/c0t3d0s0 81463 26516 54866 33% / ($free) = ($dfstring=~/^\S+\s+\S+\s+\S+\s+(\S+)/); if ($free < $minfree) { &admonish("only $free kb available, $minfree kb required"); return("only $free kb available, $minfree kb required"); } return 0; } sub checkfilehandles { # check filehandles my $rawlogfile = shift @_; my $log = basename($rawlogfile); my $dirname = dirname($rawlogfile); my $filename = "$dirname/$filedate.$log"; my $tries = 0; my $count; do { $count = 0; my $cmd = "$LSOF -f -- $filename"; if ( ! open(FH,"$cmd |") ) { &admonish("cannot run command: '$cmd'"); return("lsof failed"); } while () { $count++; } close(FH); $tries++; if ( $count ) { &admonish("attempt $tries: some processes " . "still have file $filename open"); if ( $tries > $maxtries ) { &admonish("maximum of $maxtries attempts made for " . "zero filehandles on file $filename"); return("still have filehandles"); } else { sleep($sleeptime); } } } until ( $count == 0 ); return 0; } sub gzip_logs { my $rawlogfile = shift @_; my $logdest = shift @_; my $log = basename($rawlogfile); my $dirname = dirname($rawlogfile); my $abs_file_src = "$dirname/$filedate.$log"; my $abs_file_dst = "$logdest/$filedate.$log.$gzip_extention"; if ( ! -f $abs_file_src ) { &admonish("could not find $abs_file_src - " . "did the move occur?"); return("no moved logfile"); } system("$GZIP -c < $abs_file_src > $abs_file_dst"); if ($?) { &admonish("gzip failed for $abs_file_src to " . "$abs_file_dst. exit: $?"); return("gzip failed"); } unlink $abs_file_src; if ( -f $abs_file_src ) { &admonish("log removal failed for $abs_file_src."); return("removal failed"); } return 0; } sub report { my $info = shift @_; push @reportlines, "INFO: $info\n"; print "INFO: $info\n" if $DEBUG; } sub admonish { my $admonishment = shift @_; # escalate the status, if applicable $status = "warnings" unless $status eq "errors"; push @reportlines, "WARNING: $admonishment\n"; print "WARNING: $admonishment\n" if $DEBUG; } sub scream { my $shouting = shift @_; # escalate the status $status = "errors"; my $victim; foreach $victim (@victims_pagers) { open(SCREAM,"| $MAIL -s $progname-$hostname-$status $victim"); print SCREAM "$progname-$hostname\n"; print SCREAM "$shouting\n"; close(SCREAM); } push @reportlines, "FATAL: $shouting\n"; print "FATAL: $shouting\n" if $DEBUG; } sub kick_the_bucket { my $reason = shift @_; &scream($reason); die("$reason: $!\n"); }