#!/usr/bin/perl
# $Header: /afs/cern.ch/project/afs/dev/acrontab/RCS/acrontab,v 1.44 2014/04/23 08:59:53 qba Exp $
#adm start
# $Pgm:    /afs/cern.ch/project/afs/dev/tools
# $Man:    /afs/cern.ch/project/afs/doc
# $Html    /afs/cern.ch/project/afs/doc
# $Post:   /afs/cern.ch/project/afs/dev/acrontab/acrontab.post  ## creates acrontab.sue, acrontab.sue.1
#adm end

# AFS crontab front end
# Author: R.Toebbicke, CERN/CN, rtb@mail.cern.ch
#         bja: change cron server(s), obtained from afsconf.pl
#              add -s, -L, -E  & -c option for administrators                            #adm
#              accept comments lines
#              kerberized version


use vars qw(@__C_S @__C_L);
use Getopt::Std;
use File::Temp qw(tempfile);
use vars qw($opt_h $opt_s $opt_c $opt_E $opt_L $opt_r $opt_e $opt_l) ;
use strict;
use diagnostics;

$ENV{'PATH'} = "/bin:/usr/bin:/sbin";
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};

my $plist = 'relh';            # accepted parameters
   $plist = 'c:E:L:s:relh';      # accepted parameters                                   #adm
getopts($plist) || &usage;
&usage if (defined($opt_h) || $#ARGV>$[);

# acronsrv is an alias for the active acron server
my @ACRON_SERVERS = qw(acronsrv);             # acrontab servers
#my @ACRON_SERVERS = qw(acron70);             # acrontab servers
# One letter denotes a single acrontab on the first server.
# Each subsequent letter marks the user principals which
# will have acrontabs maintained on the corrosponding @ACRON_SERVERS
#my @ACRONTAB_PARTITION_BOUNDARIES = qw(a l);
my @ACRONTAB_PARTITION_BOUNDARIES = qw(a);

# get the necessary objects: $arccmd, _cron_select
my $arccmd = "/usr/bin/arc";
my $pts = "/usr/bin/pts";
my $node;
my $cronserver = "";
my $AF_INET = 2;
if (0) {
#adm start
} elsif ($opt_s) {
  $cronserver = $opt_s;
}
if (defined $opt_c && is_a_node($opt_c)) {  # admin request to clean a node
    my ($check_node, $check_str);
    die "invalid value $node\n" if $node eq "*";
    if ($node =~ /^\d+\.\d+\.\d+\.\d+$/) {
       my(@digits)=split(/\./,$node);
       my $check_addr = pack("C4",@digits);
       ($check_node) = gethostbyaddr($check_addr,$AF_INET);
       if ($check_node) {
          $check_str = "$node ($check_node)";
       }
    } else {
       $check_node = $check_str = $node;
    }
    if (gethostbyname($check_node)) {
        print "Host $check_str still known by the name servers\n" .
              "are you sure you want to remove all acrontab entries pointing to it?\n";
        my $ans = "";
        while ( $ans !~ /^y(es?)?$/i && $ans !~ /^no?$/i)
            { print "reply no or no : "; $ans=<STDIN>; chomp $ans; }
        die "Ok, quitting\n" if ( $ans =~ /^no?$/i);
    }
    if ($cronserver) {
           &runarc("-c $opt_c");
    } else {
       foreach (l_cron_select("*")) {
           $cronserver = $_;                         # some perlism here. else it would not be visible
           &runarc("-c $opt_c");                            # in runarc!
       }
    }
} elsif ($opt_c) {                                   # admin request to clean a user
    $cronserver =  l_cron_select($opt_c) unless $cronserver;
    &runarc("-c $opt_c");
} elsif (defined $opt_L && is_a_node($opt_L)) {  # admin request to list all entries for a node
    $opt_L = "\"*\"" if $opt_L eq "*";
    if ($cronserver) {
           &runarc("-L $opt_L");
    } else {
       foreach (l_cron_select("*")) {
           $cronserver = $_;
           &runarc("-L $opt_L");
       }
    }
} elsif ($opt_L) {                                   #  admin request to list all entries for a user
    $cronserver = l_cron_select($opt_L) unless $cronserver;
    &runarc("-L $opt_L");
} elsif (defined $opt_E && is_a_node($opt_E)) {
    die "node cannot be specified as args for acrontab -E\n";
} elsif ($opt_E) {
    $cronserver =  l_cron_select($opt_E) unless $cronserver;
    &cron_edit( "-L $opt_E", "-E $opt_E" );
#adm end
} elsif ($#ARGV == $[) {
    &usage;
} elsif ($opt_l) {
    &runarc("-l");
} elsif ($opt_e) {
    &cron_edit( "-l", "");
} elsif (defined($opt_r)) {
    &runarc("-r");
} else {
    &runarc("");
}

sub cron_edit {
    my ($moreL, $moreE) = @_;
    my ($TMP_D, $tmp) = tempfile();
    my $C = &runarc("$moreL |");      # get current list
    die $C if $C =~ /^Unknown /;
    print $TMP_D $C if $C;
    close $TMP_D;
    #  we leave $tmp alone. the filesystem should better not allow anyone else
    #  to change the file!
    my $mtime = (stat($tmp))[9];

    # determine the editor to use
    # my $edit = $ENV{'VISUAL'} || $ENV{'EDITOR'} || "vi";
    my $default = "vi";
    my $edit = "";
    foreach my $e (qw(ENV{VISUAL} ENV{EDITOR} default)) {
	    my $x = eval("\$$e"); ## no critic    # the env variable's value
	    next unless $x;                       # skip if not defined
	    my ($t) = (split /\s+/, $x);          # extract command, ignore parms
	    my $u =`which $t 2>/dev/null`;        # extract actual path to command
	    die "\'$t\' (from $e) is not found or not executable  \n" unless $u;
	    $edit = $x;
	    last;
    }

    system("$edit $tmp");
    if ($mtime != (stat($tmp))[9]) {
        &runarc("$moreE < $tmp");
    }
    unlink $tmp;
    return;
}

sub runarc {
    unless ($cronserver) {
        die "cannot locate acrontab server\n" unless $cronserver = &l_cron_select();
    }
    my ($parm) = @_;
    if ($parm =~ s/\|$//) {
       return `$arccmd -h $cronserver -- crontab -K $parm`;
    }
    system "$arccmd -h $cronserver -- crontab -K $parm";
    return;
}

#adm start
sub is_a_node {
   # check if argument is a node, the character "*" or a user name
   $_[0] =~ s/^\s+//g;
   if ($_[0] eq "*") { return $node = "*"; }
   if ($_[0] =~ /([^\w\@\-\.\:])/) { die "invalid character \"$1\" in $_[0]\n"; }
   if ($_[0] =~ /^(.*)\.cern\.ch$/) { return $node = $1; }
   if ($_[0] =~ /^\d+\.\d+\.\d+\.\d+$/) { return $node = $_[0]; }
   # not a node. a user then ?
   die "unknown user $_[0]\n" unless `$pts exam $_[0] 2>/dev/null` =~ / id: \d/;
   return;
}
#adm end

sub usage {
    print <<EOFusage
Usage: $0 [ -l | -e | -r | [ <file ] ]
   -l     : list current acron jobs
   -e     : modify list of acron jobs
   -r     : remove all acron jobs
   < file : replace all acron jobs from list in <file>
          : replace all acron jobs from list typed in (^D to end typing)
EOFusage
    ;
#adm start
    print <<EOFusage
   Admin commands:
   -L  user|node   : list current acron jobs for <user> or <node>
   -E  user        : modify list of acron jobs for <user>
   -r  user|node   : remove all acron jobs for <user> or <node>

   node must be specified a a fully qualified nodename, i.e. lxplus226.cern.ch
EOFusage
    ;
#adm end
exit(1)
}

sub l_cron_select
{
   my $arc_server = $ACRON_SERVERS[0];      # quick fix for a problem with default $arc_server (==afsdb1)
##print "arc server=$arc_server, cron servers=(@ACRON_SERVERS), split at (@ACRONTAB_PARTITION_BOUNDARIES)\n";
   my $princ;
   if (@_) {
      $princ = shift @_;
      return (@ACRON_SERVERS) if $princ eq "*";
   } else {
     my($user) = getpwuid($<);
#    why use arc -h $arc_server whoami and not just parse `tokens.krb` or `klist` ?
#    well, when not used inside an AFS server, _cron_select is used by acrontab. On ANY server
#    'arc' must be installed, else acrontab cannot work, but tokens.krb might just not be there!
#    2 additionnal problems here:
#    - perl fails the arc command if we are only partially root, e.g. called from a script with su bit
#    - we have to force STDIN for arc whoami, else it will "eat" any pending input,
#      as in "acrontab < list". But then arc may forget to return something! So better retry
     my $R = $<;
     if ($< != 0 and $> == 0) { $< = $>; }   # better be totally root in only partially so
     my $i = 10;                             # prepare for some number of retries
     my $answ;
     my($klog) = "/usr/sue/bin/kinit";
     while ( $i-- ) {
        $answ = `$arccmd -h $arc_server whoami </dev/null 2>&1`;
        unless ($answ) {
           ## print "arc did not return anything, retrying\n";
           sleep 1;
           next;
        }
        elsif ( $answ =~ /:/ ) {
           if ($answ =~ /cannot authenticate/) {
              die "No kerberos token. Please use $klog to get one\n";
           } else {
              print "error in arc command, retrying.  Error message:\n$answ";
              sleep 1;
              next;
           }
        }
        last;
     }
     $< = $R;
     ($princ) = $answ =~ /^([a-z_][\w.-]+)\@/m;
     $princ =~ s/\.$//;
     unless ($princ) { die "Unable to establish kerberos identity after several attempts. Giving up\n"; }
     print STDERR "warning: you are logged-in as $user and klog'ed as $princ\n" ,
                  " we'll process acrontab entries for $princ\n\n" unless ($user eq $princ);
   }
   my($firstl) = $princ =~ /^(.)/;
   my $i = 0; foreach (@ACRONTAB_PARTITION_BOUNDARIES) { last if $firstl lt $_; $i++; }
   return $ACRON_SERVERS[$i-1];
}




__END__

=head1 NAME

acrontab - submit AFS cron jobs

=head1 SYNOPSIS

 acrontab
 acrontab -l
 acrontab -e
 acrontab -r

=head1 DESCRIPTION

B<acrontab> is used to create crontab entries for AFS-authenticated cron
jobs. Entries are added to a table on one or several central B<cron>
servers. The requested command is executed on a user-specified target
machine, equipped with a fresh AFS token of standard lifetime.

B<acrontab> entries are read from standard input if no arguments are
specified.

=head1 OPTIONS

=over 4

=item B<-e>

Invokes  the preferred editor ($VISUAL environment variable, or
$EDITOR, or vi) on a temporary file containing user's  existing
B<crontab> entries. When leaving the editor and the file has been
changed, the new B<crontab> entries replace the existing ones.

=item B<-l>

Lists the user's current crontab entries.

=item B<-r>

Removes B<all> the user's crontab entries

=item B<without arguments>,
Replaces B<all> user's cron entries with
text read from standard input (ended by ^D == crtl-D)

I<note> that contrary to B<crontab>, B<acrontab> doesnot have a <file> argument.
to read entries from a file, make it appear as STDIN, through B<acrontab < filename>.

=back

=head1 ENTRY FORMAT

Each cron job is represented by a single-line entry. Comments (lines
starting with a # character) are accepted.  B<acrontab> entries are similar
to standard Unix System V crontab entries. The main difference  is
an  additional field  preceeding the command field, specifying the IP
host name of the node the job is to be executed on.

An B<acrontab> entry has the form:

=over 4

=item B<Min Hour DayOfMon Mon WeekDay Host Command>

=back

where each of the first five fields specify an element of the time at
which the job is to be run:

=over 4

=item B<Min>  (0-59)

specifies the minute

=item B<Hour>  (0-23)

specifies the hour

=item B<DayOfMon>  (1-31)

specifies the day of the month

=item B<Mon>  (1-12)

specifies the month.

=item B<WeekDay>  (0-6)

for Sunday=0 through Saturday=6, specifies the weekday.

=item B<Host>  (valid  IP name)

specifies the host on which the job is to be run.

=item B<Command>  (valid /bin/sh command line)

The command is executed on the
remote host under the user's identity. It is given a new AFS
token, and, if the target machines supports it, a  kerberos5
ticket. The working directory is the user's home directory.

=back

=head1 PROPERTIES

The job is scheduled as soon as B<all> the first five fields match.

Any of the second through to the fifth field can be specified as a "*"
(asterisk), meaning all valid values. A "*" is not allowed in the
first field.

Any of the second through to the fifth field can contain a comma-separated
list of allowable values, meaning the field matches on any of
these values.

B<ranges> and B<steps>, as introduced in newer versions of B<crontab>, are
not supported.

No check is made whether the DayOfMon and Mon entries specify a meaningful date,
e.g. specifying "31 2" for Februray 31st is valid  but meaningless.

=head1 ENVIRONMENT VARIABLES

=over 4

=item B<VISUAL>
the preferred variable to oveeride the default editor in B<acrontab -e>

=item EDITOR
a deprecated variable to oveeride the default editor in B<acrontab -e>

=back

=head1 PREREQUISITES

The ARC facility must be installed and configured on the target node.

In order to use B<acrontab>, the user must a hold a valid Kerberos ticket
in addition to the AFS token. Normally this is obtained using standard
AFS login.

=head1 SEE ALSO

crontab(1)

=cut
