#!/usr/bin/perl 
#!/usr/local/bin/perl 
use Getopt::Std;
use Sys::Hostname;
use lib ".";
use cmconfig;

######################################################################
#
# cmupdate.pl - Updates the /etc/password file on a cluster lead machine.
#
# This script updates the student entries in the /etc/passwd file on
# the lead machine in a cluster. You can use this script to add and
# delete individual student entries (-a and -d flags), add and delete
# a batch of students from a file containing one Andrew ID per line
# (-a and -A flags), and reset the file, deleting all student entries.
#
# The script performs all of its updates as a single transaction, in
# the sense that it either makes all or none of the requested changes.
# Before it updates the password file, it creates a backup copy of the
# old one in the current directory in a file called passwd.PID, where
# PID is the process ID of the process.
#
# The password file consists of a sequence of permanent staff entries,
# followed by a special boundary entry for user $LAST_STAFF, followed
# by temporary student entries that change each semester.
#
# Configuration constants are defined in cmconfig.pm. 
#
# The password file on the lead machine is mirrored on all the other
# cluster machines. To update the password files on the cluster, use
# this script to update the password file on the lead machine, and
# then wait for the changes to be mirrored everywhere else.
#
# Accounts for staff members must always appear before user $LAST_STAFF
# and are always created manually.
#
# Use the cmcheck.pl script to check the correctness of the passwd file
# and to print the status of each student account.
# 
######################################################################

$| = 1;      # Autoflush output on every print statement
umask(0077); # Any files created by script are readable only by user

#
# usage - print help message and terminate
#
sub usage {
    printf STDERR "$_[0]\n";
    printf STDERR "Usage: $0 [-hR -a <usr> -A <file> -d <usr> -D <file> -c <course>]\n";
    printf STDERR "Options:\n";
    printf STDERR "  -h          Print this message.\n";
    printf STDERR "  -R          Reset, delete ALL student entries.\n";
    printf STDERR "  -a <usr>    Add single user for <course>.\n";
    printf STDERR "  -A <file>   Add batch of users in <file> for <course>.\n";
    printf STDERR "  -d <usr>    Delete a single user.\n";
    printf STDERR "  -D <file>   Delete batch of users in <file>.\n";
    printf STDERR "  -c <course> Course number (e.g. 15-213) (required for -a and -A)\n";
    die "\n";
}


#
# aterm - abnormal termination
#
sub aterm {
  my $msg = shift;     # message to print
  die "$msg\nAbnormal termination. No updates were made to the password file ($PASSWD_FILE)\n";
}

#
# nterm - normal termination
#
sub nterm {
  print "Normal termination. Password file ($PASSWD_FILE) successfully updated.\n";
  exit;
}

#
# newentry - construct a new student password entry
#
sub newentry {
    my $username = shift; # argv[0]
    my $course = shift;   # argv[1]

    my $homedir = "$PATH_PREFIX/$username/$course";
    my $userid, $fullname;

    $a_line = `grep ^$username: $ANDREW_PASSWD`;
    if (!$a_line) {
	aterm "ERROR: Couldn't find $username in Andrew password file.";
    }

    ($userid, $fullname) = (split(':', $a_line))[2, 4]; 

    ####################################################################### 
    # Hack to deal with sad fact that some Linux systems can't 
    # handle userIDs over 65535
    #######################################################################
    #if ($userid >= 1<<16) {
    #	$userid -= 1<<16;
    #} 
    # Not needed anymore - AGN 8-31-2004

    if ($userid == 0) {
	$userid = 1;
    }

    return join(":", $username, "*", $userid, $DEFAULT_GID, 
		$fullname, $homedir, $DEFAULT_SHELL);
}

##############
# Main routine
##############

#
# Initialize some key globals
@STAFF = ();     # Constant list of staff entries
%STUDENTS = ();  # Hash of student entries 

# 
# Parse and check the command line arguments
#
getopts('hRa:A:d:D:c:');
if ($opt_h) {
    usage("");
}


if (!($opt_R or $opt_a or $opt_A or $opt_d or $opt_D)) { 
    usage("You must call the script with at least one of -a, -A, -d, -D, or -R."); 
}

if (($opt_a or $opt_A) and !$opt_c) {
    usage "ERROR: The -c arg is required for the -a and -A args.";
}

# Is the course in the -c argument a valid course for this cluster
$COURSE = ();
foreach $coursenum (@COURSE_LIST) {
    $COURSE{$coursenum} = $coursenum;
}
if ($opt_c and !$COURSE{$opt_c}) {
    $coursestr = join(",", @COURSE_LIST);
    aterm "ERROR: Argument to -c ($opt_c) is not a valid course number.\nExpecting one of the following: $coursestr";
}

# Is the default shell path valid
if (!-x $DEFAULT_SHELL) {
    aterm "ERROR: Default shell ($DEFAULT_SHELL) is not executable."; 
}

# Are we running on the cluster lead machine?
if ($LEAD_HOST ne "" and hostname() ne $LEAD_HOST) {
    aterm "ERROR: You must run this script on lead machine ($LEAD_HOST).";
}

# Does a readable password file exist?
if (!-e $PASSWD_FILE or !-r $PASSWD_FILE) {
    aterm "ERROR: Readable password file ($PASSWD_FILE) not found.";
}


#####################################################################
# Read in the entire cluster machine password file. Entries before and 
# up to the $LAST_STAFF user entry are assumed to be staff, those after 
# are assumed to be students.
#####################################################################

open (INFILE, $PASSWD_FILE)
    or aterm "$0: ERROR: could not open password file ($PASSWDFILE) for reading.";
$in_student_section = 0;
while ($line = <INFILE>) {
    chomp($line);

    ($username, $x, $userid, $groupid, $fullname, $homedir, $shell) =  
	split(':', $line); 

    if (!$in_student_section) {
	push (@STAFF, $line);
    }
    else {
	$STUDENTS{$username} = $line;
    }

    if ($username =~ /^$LAST_STAFF/) {
	$in_student_section = 1;
    } 
}	
close (INFILE);
if (!$in_student_section) {
    aterm "ERROR: Did not find the expected last staff entry ($LAST_STAFF).";
}

######################################################
# Perform updates on the entries in STAFF and STUDENTS
######################################################

#
# Reset the password file (-R)
# 
if ($opt_R) {
    print "Reseting password file ($PASSWD_FILE)\n";
    %STUDENTS = ();
}

#
# Delete an individual student entry (-d)
#
if ($opt_d) {
    if (!$STUDENTS{$opt_d}) {
	aterm "ERROR: Could not find user ($opt_d).";
    }
    else {
	delete($STUDENTS{$opt_d});
	print "Deleting student entry ($opt_d).\nb";
    }
}

#
# Delete a batch of entries (-D)
#
if ($opt_D) {
    open (INFILE, $opt_D)
	or aterm "$0: ERROR: could not open batch file ($opt_D) for reading.";

    while ($user = <INFILE>) {
	if ($user =~ /^\s+/) { # skip blank lines
	    next;
	}
	chomp ($user);
	if (!$STUDENTS{$user}) {
	    aterm "ERROR: Could not find user ($user) in $PASSWD_FILE.";
	}
	else {
	    delete($STUDENTS{$user});
	    print "Deleting student entry ($user)\n";
	}
    }
    close (INFILE);
}

#
# Add an individual student entry (-a)
#
if ($opt_a) {

    if ($STUDENTS{$opt_a}) {
	aterm "ERROR: Entry for ($opt_a) already exists.";
    }
    $STUDENTS{$opt_a} = newentry($opt_a, $opt_c);
    print "Adding student entry ($opt_a) for course ($opt_c)\n";
}

#
# Add a batch of entries (-A)
#
if ($opt_A) {
    open (INFILE, $opt_A)
	or aterm "$0: ERROR: could not open batch file ($opt_A) for reading.";

    while ($user = <INFILE>) {
	if ($user =~ /^\s+/) { # skip blank lines
	    next;
	}
	chomp ($user);
	if ($STUDENTS{$user}) {
	    aterm "ERROR: Entry for user ($user) already exists.";
	}
	$STUDENTS{$user} = newentry($user, $opt_c);
	print "Adding student entry ($user) for course ($opt_c)\n";
    }

    close (INFILE);
}


######################################
# Everything is OK, Commit the changes 
######################################

# Ma
system("cp $PASSWD_FILE ./passwd.$$") == 0 
    or aterm "ERROR: Couldn't make backup copy of password file ($PASSWD_FILE).";
print("Made backup copy of old $PASSWD_FILE in ./passwd.$$\n"); 


open (OUTFILE, ">$PASSWD_FILE")
    or aterm "$0: ERROR: could not open passwd file ($PASSWDFILE) for writing.";
foreach $line (@STAFF) {
    print OUTFILE "$line\n";
}
foreach $line (sort values %STUDENTS) {
    print OUTFILE "$line\n";
}
close (OUTFILE);

nterm();
