# -*-perl-*-

# $Id: Crontab.plm,v 1.9 2005/11/21 20:52:37 thomas Exp $

# What package to handle RPMs

# $Log: Crontab.plm,v $
# Revision 1.9  2005/11/21 20:52:37  thomas
# Oops. Delete empty crontabs for managed users [CSL #287304]
#
# Revision 1.8  2005/09/21 14:06:21  thomas
# get file mode stuff right.
# put new lines in the right places
#
# Revision 1.7  2005/09/14 20:08:20  thomas
# Allow parsing of crontabs separately from applying new tabs
# for AllHosts/Crontab report
#
# Revision 1.6  2004/05/24 14:45:14  thomas
# Remove unused crontabs
#
# Revision 1.5  2004/05/24 14:27:13  thomas
# Warn about crontabs I think I should delete...
#
# Revision 1.4  2004/03/05 15:12:47  thomas
# cconf->what
#
# Revision 1.3  2004/01/13 16:48:31  thomas
# catch-up 1/13/04
#
# Revision 1.2  2003/12/15 16:28:34  thomas
# check-in for 0.1.1
#
# Revision 1.1.1.1  2003/12/02 14:47:35  thomas
# Initial version before export to 0.1.1
#

=head1 NAME

What::Node::Crontab - Manage user and system crontab entries

=head1 DESCRIPTION

This subtree manages both user and system (vixie cron) crontabs.
There are two B<Crontab> subtrees; B<user>, which manages user
crontabs, and B<admin> which manages system crontabs.

User crontab entries exist in nodes with path
B</Crontab/user/I<user>/I<key>>, where I<user> is the name of the
user, and I<key> is a key that uniquely identifies the crontab entry
(I.e.  I<Key> must be unique to that I<user>).

System crontab entries exist in nodes with path
B<</Crontab/admin/I<foo>/I<bar>/.../I<qux>/I<key>>.  The entries
I<foo>/I<bar>/.../I<qux> identify the path to the system crontab, and
I<key> once again uniquely identifies the crontab entry within that
system crontab.

There are two types of attribute sets that can be specified, depending
on whether the node containing those attributes corresponds to a
crontab B<file> or an B<entry> in a crontab file.  For crontab file
nodes, (e.g. B</Crontab/user/I<user>> or
B</Crontab/admin/I<foo>/I<bar>/.../I<qux>>), any attributes are
assumed to be environment variables for the crontab file.  For crontab
entries, B<when> and B<command> attributes are placed in the crontab
file.  In addition, a B<user> attribute can be specified for admin
crontabs, which is placed in the user field of the admin crontab.

This module requires that the base directory for user crontabs be
present as the B<basedir> attribute of the B</Crontab/users> node.
This can be specified as:

<node path="/Crontab/users" basedir="/var/spool/cron" />

=head1 EXAMPLES

Create an entry in the user B<mirror>'s crontab to run a program

<node path="/Crontab/user/mirror/run-mirror" when="30 23 * * *"
command="( /s/std/bin/runauth -a /u/m/i/mirror/bin/run-mirror.sh -d
2>&1 ) | /bin/mail -s "mirror results" mirror-admin > /dev/null" />

Create B<MAILTO> and B<SHELL> environment variables in the mirror crontab:

<node path="/Crontab/user/mirror" MAILTO="lab@cs.wisc.edu"
SHELL="/bin/tcsh" />

Create the hourly entry in the system crontab:

<node path="/Crontab/admin/etc/crontab" SHELL="/bin/bash" PATH="/sbin:/bin:/usr/sbin:/usr/bin" MAILTO="root" HOME="/" />
<node path="/Crontab/admin/etc/crontab/hourly" when="01 * * * *"
user="root" command="/etc/cron.hourly" />

=cut

package What::Node::Crontab;

use strict;
use FileHandle;
use Data::Dumper;

use base qw(What::Node);

our %opts = ();

# Set the priority for this module in $::
$::module_priority{'Crontab'} = 20;

# Constants
my $USERCRONTAB = 0;
my $ADMINCRONTAB = 1;

###############################################################################
# Apply the current configuration

sub apply {
  my $class = shift;
  my $section = shift;
  my $what  = shift;
  my $sname = shift;
  my $cname = shift;

  my $tabs = {};

  $class->parse_tabs ($section, $tabs);

  $class->apply_tabs ($tabs);

  return 1;
}

sub parse_tabs {
  my $class = shift;
  my $section = shift;
  my $tabs = shift;

  my ($users, $admin);
  if (defined ($users = $class->find_node ($section, '/users'))) {
    $users->parse_users ($class, $section, $tabs);
  }
  if (defined ($admin = $class->find_node ($section, '/admin'))) {
    $admin->parse_admin ($class, $section, $tabs);
  }

}


sub apply_tabs {
  my $class = shift;
  my $tabs = shift;

  my ($tab, $file, $text);

  foreach $file (sort keys %$tabs) {
    $tab = $tabs->{$file};
    $text = $class->tab_to_text ($tab, 0);
    if (length $text) {
      $class->check_and_replace_content (
					 $file,
					 $text,
					 0,
					 0,
					 $tab->{uid},
					 $tab->{gid},
					 $tab->{mode}
					);
    } elsif (-e $file) {
      # Delete the crontab
      $class->err ("/Crontab: removing %s\n", $file);
      $class->remove_file ($file);
    }
  }
}


###############################################################################
# Parse what crontab data for users

sub parse_users {
  my $users = shift;
  my $class = shift;
  my $section = shift;
  my $tabs = shift;

  my ($children, $child, $usertab, $basedir, $uid, $gid, $mode);

  if (!defined ($basedir = $users->getattrs ($section, attr => 'basedir'))) {
    $class->err ("No user base directory specified for crontab configurator!\n");
    return 0;
  }

  $children = $users->getchildren ($section);
  foreach $child (sort keys %$children) {
    $users->debug (3, "Checking user crontab for $child\n");
    ($uid, $gid) = (getpwnam ($child))[2,3];
    $usertab = 
      $children->{$child}->maketab ($section, $USERCRONTAB, $uid, $gid, 0600);
    # next unless defined ($usertab->{tabs})
    #   && ref ($usertab->{tabs}) eq 'HASH'
    #     && %{$usertab->{tabs}};
    $tabs->{"$basedir/$child"} = $usertab;
  }
}


###############################################################################
# Parseadmin crontab(s).  Identify a crontab file node as the parent of a
# node with all the right attributes.  Try to be a little efficient.

sub parse_admin {
  my $admins = shift;
  my $class = shift;
  my $section = shift;
  my $tabs = shift;

  # The list of crontabs that I've already replaced.
  my $done = {};

  my ($children, $child);
  $children = $admins->getchildren ($section);
  foreach $child (sort keys %$children) {
    $children->{$child}->parse_admin_r ($section, $admins, $done,
					$admins, '', $child, $tabs);
  }
}

sub parse_admin_r {
  my $node = shift;
  my $section = shift;
  my $admins = shift;
  my $done = shift;
  my $parentnode = shift;
  my $parentpath = shift;
  my $myname = shift;
  my $tabs = shift;

  my ($children, $child, $attrs, $admintab, $uid, $gid, $mode);
  $children = $node->getchildren ($section);
  if (keys %$children) {
    foreach $child (sort keys %$children) {
      $children->{$child}->parse_admin_r ($section, $admins, $done,
					  $node, "$parentpath/$myname",
					  $child, $tabs);
      # If I'm a crontab file node, I may have just been built.  Find out
      # now and be done.
      $done->{"$parentpath/$myname"} and return 1;
    }
  } else {
    # This may be a crontab entry (no kids).  Check if
    # It has the right attributes.
    $attrs = $node->getattrs ($section);
    if (defined ($attrs->{when}) &&
	defined ($attrs->{command}) &&
	defined ($attrs->{user})) {
      # Egad.  $node is a crontab entry.  Make a new crontab from $parentnode
      $node->debug (3, "Checking admin crontab $parentpath\n");
      $admintab = $parentnode->maketab ($section, $ADMINCRONTAB, 0, 0, 0600);
      # Now, mark it as done
      $done->{$parentpath} = 1;
      next unless defined ($admintab->{tabs})
      && ref ($admintab->{tabs}) eq 'HASH'
        && %{$admintab->{tabs}};
      $tabs->{$parentpath} = $admintab;
    }
  }
}


# Generate a crontab in memory.  $node points to the crontab file node.
# If $admintab is set, include (and require) the user attribute.

sub maketab {
  my $node = shift;
  my $section = shift;
  my $admintab = shift;
  # Get defaults for uid, gid, mode from caller
  my $uid = shift;
  my $gid = shift;
  my $mode = shift;

  my ($tabs, $tkey, $attrs, $attr);

  my $crontab = {
		 uid  => $uid,
		 gid  => $gid,
		 mode => $mode,
		};


  # Get the environment entries
  $attrs = $node->getattrs ($section);
  foreach $attr (sort keys %$attrs) {
    if ($attr =~ /^(uid|gid|mode)$/) {
      $crontab->{$attr} = $attrs->{$attr};
    } else {
      $crontab->{env}->{$attr} = $attrs->{$attr};
    }
  }
  $crontab->{admintab} = $admintab;
  # Fix up mode
  $crontab->{mode} = oct ($crontab->{mode})
    if (defined ($crontab->{mode}) && $crontab->{mode} =~ /^0/);

  # Add the entries
  $tabs = $node->getchildren ($section);
  foreach $tkey (sort keys %$tabs) {
    $attrs = $tabs->{$tkey}->getattrs ($section);
    next unless defined $attrs->{when};
    next unless defined $attrs->{command};
    if ($admintab) {
      next unless defined $attrs->{user};
    }
    foreach $attr ('when', 'command', 'user', 'SkipReport', 'comment') {
      $crontab->{tabs}->{$tkey}->{$attr}
	= exists ($attrs->{$attr})?$attrs->{$attr}:undef;
    }
    $crontab->{tabs}->{$tkey}->{SkipReport}
	= exists ($attrs->{SkipReport})?$node->checkbool($attrs->{SkipReport}):0;
  }
  $crontab;
}


# Convert the crontab nodes to text.  If this is for the report,
# skip the preamble and any nodes with 'SkipReport' set.

sub tab_to_text {
  my $node = shift;
  my $tab = shift;
  my $report = shift;

  my $basetab = '';

  $basetab = <<__Eof__
###
# This crontab was generated automatically by What::Node::Crontab
# Hand-made changes will be lost!!
###
__Eof__
    unless $report;

  my $crontab = $basetab;
  my $numattrs = 0;
  my ($attr, $key, $entry);

  foreach $attr (sort keys %{$tab->{env}}) {
    $crontab .= "$attr=$tab->{env}->{$attr}\n";
    $basetab .= "$attr=$tab->{env}->{$attr}\n";
    $numattrs++;
  }

  foreach $key (sort keys %{$tab->{tabs}}) {
    $entry = $tab->{tabs}->{$key};
    if (!$report || !$entry->{SkipReport}) {
      # Include this entry in the text version of the crontab
      $crontab .= "\n";
      $crontab .= "# $entry->{comment}\n" if defined $entry->{comment};
      if ($tab->{admintab}) {
	$crontab .= "$entry->{when} $entry->{user} $entry->{command}\n";
      } else {
	$crontab .= "$entry->{when} $entry->{command}\n";
      }
    }
  }
  ($crontab eq $basetab)?'':$crontab;
}


###############################################################################
#
# For Red Hat, do something like:
#
# % what generate Crontab /var/spool/cron /etc/crontab /etc/cron.d

sub generate {
  my ($class, $section, $what, $user_crontab_dir, @admin_crontab_files) = @_;

  $class->generate_users ($section, $what, $user_crontab_dir);

  $class->generate_admins ($section, $what, @admin_crontab_files);

  return 1;
}

sub generate_users {
  my ($class, $section, $what, $user_crontab_dir) = @_;

  my ($dh, $de, $fh, $line, $count);
  my $basedir = (defined ($::opts{root}) ? $::opts{root} : '')
    . $user_crontab_dir;

  defined ($dh = new DirHandle $basedir) or return 0;
  while (defined ($de = $dh->read)) {
    $de =~ /^\./ and next;
    if ((defined (getpwnam ($de)) || defined ($::opts{root}))
	&& defined ($fh = new FileHandle "$basedir/$de", 'r')) {
      $count=1;
      while (defined ($line = <$fh>)) {
	$line =~ /^\s*\#/ and next;
	if ($line =~ /^\s*(\S+\s+\S+\s+\S+\s+\S+\s+\S+)\s+(.*)$/) {
	  $class->setattrs (
			    $section,
			    {
			     when => $1,
			     command => $2,
			    },
			    "/users/$de/$count",
			    1,
			   );
	  $count++;
	} elsif ($line =~ /\^\s*(\S+)\s*\=(.*)$/) {
	  $class->setattrs (
			    $section,
			    {
			     $1 => $2,
			    },
			    "/users/$de",
			    1,
			   );
	}
      }
    }
  }
}

sub generate_admins {
  my ($class, $section, $what, @admin_crontab_files) = @_;

  my ($dh, $de);

  foreach (@admin_crontab_files) {
    if (-d $_) {
      $dh = new DirHandle $_;
      while (defined ($de = $dh->read)) {
	$de =~ /^\./ and next;
	(-f "$_/$de") and $class->generate_admin_file ($section, $what, "$_/$de");
      }
    } else {
      (-f $_) and $class->generate_admin_file ($section, $what, $_);
    }
  }
}

sub generate_admin_file {
  my ($class, $section, $what, $file) = @_;

  my ($fh, $line);
  my $count=1;
  if (defined ($fh = new FileHandle "$file")) {
    while (defined ($line = <$fh>)) {
      $line =~ /^\s*\#/ and next;
      if ($line =~ /^\s*(\S+\s+\S+\s+\S+\s+\S+\s+\S+)\s+(\S+)\s+(.*)$/) {
	$class->setattrs (
			  $section,
			  {
			   when => $1,
			   user => $2,
			   command => $3,
			  },
			  "/admin$file/$count",
			  1,
			 );
	$count++;
      } elsif ($line =~ /^\s*(\S+)\s*\=(.*)$/) {
	$class->setattrs (
			  $section,
			  {
			   $1 => $2,
			  },
			  "/admin$file",
			  1,
			 );
      }
    }
  }
}


###############################################################################
1;
