###
###  Copyright 2000-2007 University of Illinois Board of Trustees
###  All rights reserved. 
###
###  PSGConf::Action::Crontab - crontab action type for psgconf
###
###  Campus Information Technologies and Educational Services
###  University of Illinois at Urbana-Champaign
###


package PSGConf::Action::Crontab;

use strict;

use File::Basename;
use File::Compare;
use File::Copy;
use File::Path;

use Text::Diff ();		# Use () so we do not get diff() redefinition errors

use PSGConf::Action;

use PSGConf::Util;
use Cwd;

our @ISA = qw(PSGConf::Action);


###############################################################################
###  constructor
###############################################################################

sub new
{
	my ($class, %opts) = @_;

	die "PSGConf::Action::Crontab->new(): entries attribute missing\n"
		if (!exists($opts{entries}));

	$opts{user} = 'root'
		if (!exists($opts{user}));

	$opts{name} = "$opts{user} crontab"
		if (!exists($opts{name}));

	return PSGConf::Action::new($class, %opts);
}


###############################################################################
###  check() method
###############################################################################

sub check
{
	my ($self, $psgconf) = @_;
	my ($entry, $entries, $cmd, $line, $skip_flag, $olddir);

	### set tmpfile name
	$self->_set_tmpfile($psgconf);

	### open tempfile
	if (!open(TMPFILE, '>' . $self->{tmpfile}))
	{
		warn "\n\t!!! open('>$self->{tmpfile}'): $!\n";
		return -1;
	}

	### print banner
	print TMPFILE <<EOF;
###
### crontab for $self->{user}
###
### Automatically generated by psgconf - DO NOT EDIT!
###
EOF

	### print crontab entries
	foreach $entry (sort { $a->{command} cmp $b->{command} } @{$self->{entries}})
	{

		### If this is a Vixie cron entry, ignore it here
		if ( ! exists $entry->{type} ||
			$entry->{type} ne 'vixie' ) {
			$entries++;
			print TMPFILE join(' ', (map {
				(defined($entry->{$_}) ? $entry->{$_} : '*');
			} qw(minute hour dom month dow)));

			print TMPFILE "\t" . $entry->{command} . "\n";
		}
	}

	### close tmpfile
	close(TMPFILE)
		|| die $0 . ": close('$self->{tmpfile}'): $!";

	### Since we did not write any entries to the crontab
	### return with no entries, like we are not managing
	### the crontab file at all.
	if ( ! $entries ) {
		unlink($self->{tmpfile});
		return 0;
	}

	### grab copy of original file (but move to what we think
	### is a world readable/executable directory so the su
	### does not fail.
	$olddir = getcwd;
	chdir (dirname($psgconf->{tmpdir}));
	$self->{orig_file} = $self->{tmpfile} . '.orig';
	$cmd = '';
	$cmd .= "su $self->{user} -c \""
		if ($self->{user} ne 'root');
	$cmd .= 'crontab -l';
	$cmd .= '"'
		if ($self->{user} ne 'root');
	$cmd .= ' 2>/dev/null |';

	open(ORIGFILE, ">$self->{orig_file}")
		|| die $0 . ": open('>$self->{orig_file}'): $!";
	open(CRONTAB_CMD, $cmd)
		|| die $0 . ": open('$cmd'): $!";

	while ($line = <CRONTAB_CMD>)
	{
		$skip_flag = 1
			if ($. == 1
			    && $psgconf->data_obj('platform')->match('linux')
			    && $line =~ m/^# DO NOT EDIT THIS FILE -/);

		next
			if ($skip_flag
			    && $. <= 3);

		print ORIGFILE $line;
	}

	chdir ($olddir);
	close(ORIGFILE)
		|| die $0 . ": close('$self->{orig_file}'): $!";
	if (!close(CRONTAB_CMD) && $! != 0)
	{
		die $0 . ": close('$cmd'): $!";
	}

	### check for differences
	if (compare($self->{orig_file}, $self->{tmpfile}) != 0)
	{
		$self->{changed} = 1;
		return 1;
	}

	### no change - unlink tempfile
	unlink($self->{tmpfile});

	return 0;
}


###############################################################################
###  diff() method
###############################################################################

sub diff
{
	my ($self, $psgconf) = @_;
	my ($cmd, $rc);

	print "###############################################################################\n";
	print "### DIFF FOR $self->{name}\n";
	print "###############################################################################\n";

	print Text::Diff::diff $self->{orig_file},  $self->{tmpfile};

	print "\n";

	unlink($self->{orig_file});
}


###############################################################################
###  do() method
###############################################################################

sub do
{
	my ($self, $psgconf) = @_;
	my ($cmd, $res, $crontab_tmpfile, $olddir);

	$res=1;

	### When updating the crontab for a non-root user, the crontab
	### command running as that user will not be able to read the
	### file from psgconf's tmpdir, since it's owned by root and
	### mode 0700.  To work around this, we copy the file to another
	### directory and make it owned by the user.
	
	### This code originally tried to get around this problem with
	### "crontab < filename", but it turns out that that doesn't
	### work with vixie cron.

	if ($self->{user} ne 'root')
	{
		$crontab_tmpfile = $psgconf->{tmpdir} . '.1';
		my $umask = umask 022;
		mkpath ($crontab_tmpfile, 0, 0755);
		umask $umask;
		$crontab_tmpfile .= '/' . basename $self->{tmpfile};
		copy($self->{tmpfile}, $crontab_tmpfile);
		chown($self->{uid}, -1, $crontab_tmpfile);
		$cmd = "su $self->{user} -c \"crontab $crontab_tmpfile\"";
	}
	else
	{
		$cmd = "crontab $self->{tmpfile}";
	}

	### Jump to what we think is a world readable directory to run
	### the crontab command as possibly non root.
	$olddir = getcwd;
	chdir (dirname($psgconf->{tmpdir}));
	$res = -1
		if (&PSGConf::Util::RunCommand($cmd));

	chdir ($olddir);

	### remove the file we created for the crontab command to read
	if ($self->{user} ne 'root')
	{
		rmtree(basename($crontab_tmpfile));
	}

	### remove tmpfile if applicable
	if (-e $self->{tmpfile}
	    && $psgconf->{rm_tmpfiles}
	    && !unlink($self->{tmpfile}))
	{
		warn "\t!!! unlink('$self->{tmpfile}') : $!\n";
	}

	return ($res);
}


###############################################################################
###  documentation
###############################################################################

1;

=head1 NAME

PSGConf::Action::Crontab - root crontab action class for PSGConf

=head1 SYNOPSIS

  use PSGConf::Action::Crontab;

  $psgconf->register_actions(
		PSGConf::Action::Crontab->new(
			entries	=> [ {
					minute	=> 0,
					hour	=> 0,
					command	=> '/usr/local/sbin/nightly'
				     },
				     ...
				   ],
			user	=> 'root',
			...
		),
		...
	);

=head1 DESCRIPTION

The B<PSGConf::Action::Crontab> module provides a B<PSGConf> action class
for generating crontab files.

The B<PSGConf::Action::Crontab> class is derived from the
B<PSGConf::Action> class, but it defines/overrides the following
methods:

=over 4

=item check()

Generates the new crontab file and compares it to the existing one for
the user specified by the I<user> attribute.  Returns true if the files
differ, and false otherwise.

=item diff()

Uses the C<diff> command to display the differences between the existing
crontab and the newly generated version.

=item do()

Uses the C<crontab> command to install the newly generated crontab.

=back

In addition to the attributes supported by the B<PSGConf::Action>
class, the B<PSGConf::Action::Crontab> class supports the following
attributes:

=over 4

=item I<user>

The name of the user whose crontab is to be generated.

=item I<entries>

An anonymous array containing the entries to be placed in the user's
crontab.  Each entry is an anonymous hash containing the following
fields:

=over 4

=item I<minute>

The minute of the entry (range 0-59).  If not set, defaults to C<*>.

=item I<hour>

The hour of the entry (range 0-23).  If not set, defaults to C<*>.

=item I<dom>

The day of month of the entry (range 1-31).  If not set, defaults to C<*>.

=item I<month>

The month of the entry (range 1-12).  If not set, defaults to C<*>.

=item I<dow>

The day of week of the entry (range 0-6, with 0 being Sunday).
If not set, defaults to C<*>.

=item I<command>

The command to execute.  This field is required.

=back

=back

=head1 SEE ALSO

L<perl>

crontab(5)

L<PSGConf>

L<PSGConf::Action>

L<PSGConf::Util>

=cut




syntax highlighted by Code2HTML, v. 0.9.1