#! /usr/bin/perl -w # taterlogs - portable, simple log rotation with compression and post-commands # Copyright (C) 2001 Ed Cashin # # 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. # # original: ecashin@ping:src/script/rotatelogs # Changes: # 20010723 - added alias feature for more readable, easy config files # added command-line option to override default config file # added test mode where commands are printed instead of executed # added help option and usage info # renamed script to "taterlogs" # use strict; use File::Copy; use Getopt::Long; use vars qw($CONF_FILE $ME); $ME = $0; $ME =~ s!.*/!!; $CONF_FILE = "/usr/local/etc/taterlogs.conf"; &go; sub usage { print <<"EOUSAGE"; usage: $ME [--conf={conffile}] [--test] $ME --help options: conf specify config file, overriding default test print summary of actions instead of doing them help show this help EOUSAGE } sub go { my ($conf_file, $testing, $help_request); GetOptions('conf=s' => \$conf_file, 'test|?' => \$testing, 'help|?' => \$help_request); if ($help_request) { &usage; exit; } # ----------- use default in absence of option $conf_file = $CONF_FILE unless $conf_file; my %cmd_aliases; my $conf = &conf_by_command($conf_file, \%cmd_aliases, $testing); &rotate($conf, \%cmd_aliases, $testing); } sub rotate { die "$ME Error: missing parameter" if @_ < 3; my ($confset, $aliases, $testing) = @_; foreach my $command (keys %$confset) { my $logs = $confset->{$command}; # --------- expand command if it's an alias if (defined(my $expansion = $aliases->{$command})) { $command = $expansion; } my $did_rotate; # we only do the command if we rotated logs foreach my $log (@$logs) { $did_rotate = "yes" if &rotate_log($log, $testing); } if ($command && $did_rotate) { if (! $testing) { system($command); } else { print "$command\n"; } } } } sub rotate_log { die "$ME Error: missing parameter" if @_ < 2; my ($attrs, $testing) = @_; my $filename = $attrs->{'filename'}; return 0 unless -e $filename; # --------- return if the file is too small. # minimum size set to zero means there's no minimum. # if (my $min_k = $attrs->{'minsiz_k'}) { my $size = (stat(_))[7]; return 0 if ($size / 1024) < $min_k; } # get info about the file my $mode = (stat(_))[2]; # file type and permissions my $perms = sprintf "%04o", ($mode & 07777); my ($uid, $gid) = ((stat(_))[4], (stat(_))[5]); my $i; # ------- find one past the last numbered copy for ($i = 1; ; ++$i) { last unless ((-e "$filename.$i") || (-e "$filename.$i.gz")); } --$i; # decrement to the last numbered copy for ( ; $i; --$i) { my $next = $i + 1; next if $next > $attrs->{'n_copies'}; &move_file("$filename.$i", "$filename.$next", $testing) if -e "$filename.$i"; &move_file("$filename.$i.gz", "$filename.$next.gz", $testing) if -e "$filename.$i.gz"; } move_file("$filename", "$filename.1", $testing) or die "$ME Error: could not move $filename to $filename.1: $!"; # --------- create the new file with appropriate permissions and # ownership die "$ME Error: could not create $filename" unless &create_file($filename, $perms, $testing); warn "$ME Warning: could not set ownership to uid($uid) gid($gid)\n" unless &chown_file($uid, $gid, $filename, $testing); if ($attrs->{'compress'}) { unless (&gzip_file("$filename.1", $testing)) { die "$ME Error: gzip failed for file ($filename.1): $!"; } } return "yes"; } # ------------ instead of SIGHUP-ping everyone a million times, # save up the files that have the same post-rotation # command. # sub conf_by_command { die "$ME Error: missing parameter" if @_ < 2; my ($conffile, $aliases, $testing) = @_; open CONF, $conffile or die "$ME Error: could not open conf file($conffile): $!"; my %confset; while (defined(my $line = )) { next if $line !~ /\S/ || $line =~ /^\s*\#/; # skip blanks and comments chomp $line; if ($line =~ /(\S+)\s*=\s*(.*)/) { $aliases->{$1} = $2; print "alias: $1 = $2\n" if $testing; next; } my ($filename, $compress, $n_copies, $k_min, $command) = split ":", $line; $command = "" unless defined $command; $confset{$command} = [()] unless $confset{$command}; # ------------ use defaults if they didn't provide some values push @{ $confset{$command} }, { 'filename' => $filename, 'compress' => (defined $compress ? $compress : 0), 'n_copies' => (defined $n_copies ? $n_copies : 20), 'minsiz_k' => (defined $k_min ? $k_min : 0), 'command' => (defined $command ? $command : ""), }; } close CONF; return \%confset; } sub move_file { die "$ME Error: missing parameter" if @_ < 3; my ($old, $new, $testing) = @_; if (! $testing) { return move $old, $new; } else { print "move $old --> $new\n"; return 1; } } sub gzip_file { die "$ME Error: missing parameter" if @_ < 2; my ($filename, $testing) = @_; return ! system "gzip $filename" unless $testing; print "gzip $filename\n"; return 1; } sub chown_file { die "$ME Error: missing parameter" if @_ < 4; my ($uid, $gid, $filename, $testing) = @_; return (chown($uid, $gid, $filename) == 1) unless $testing; print "chown $uid:$gid $filename\n"; return 1; } sub create_file { die "$ME Error: missing parameter" if @_ < 3; my ($filename, $perms, $testing) = @_; my $command = "set -e; umask 077; > $filename && chmod $perms $filename"; return ! system $command unless $testing; print "$command\n"; return 1; }