#! /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 = <CONF>)) {
	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;
}
