From schemers@jessica.stanford.edu Tue Mar 10 23:25:04 1992
To: Perl-Users@fuggles.acc.Virginia.EDU
From: schemers@jessica.stanford.edu (Roland Schemers)
Subject: add_user.pl
Date: 26 Feb 92 17:37:22 GMT
SUB: add_user.pl
SUM: schemers@jessica.stanford.edu (Roland Schemers)->Perl-Users@fuggles.acc.Virginia.EDU

I wrote a perl script for doing this while at Oakland University. I'll
e-mail to it you. It allows you to define the following defaults:

 o  default group
 o  default shell
 o  default quota
 o  list of  directories to use as parent home directories. It will then
    add the user to the directory with the lowest link count, basically
    round robin.

 o  it will directly insert a new user into the hash password file
    (/etc/passwd.{pag,dir}) if these files exist.

 o  it adds the quota by calling the quota system call from perl

 o  the next uid is taken by doing a 'tail -1 /etc/passwd', so your
    passwd file should be sorted by uid (at least the last entry
    should be the highest).

We have used it to add over 2000 students, and has performed quite well.

It has the following options:

 usage:  add_user [-options ...] username

where options include:                                    Defaults -
    -dir      dir             parent directory            least used disk
    -full     "full name"     full name of new user       username
    -group    group           group of new user           $def_group
    -password password        password of new user        username (don't use!)
    -quota    quota           disk quota in kbytes        $def_quota
    -shell    login shell     login shell of new user     $def_shell

All options can be abbreviated up to one letter.
 ----------------------------------------------------------------------

I tried to comment out or take out an OU specifiy stuff. The version I just
posted to alt.sources still had a line to check and make sure it was only
called from my account registration program, so I took that out of this
version. It should work standalone without any problem.

There is a section that creates a .forward file that I commented out.

If I get time I'll take suggestions for improvements and clean it up a
little.

 ----------------------------------------------------------------------

#!/usr/local/bin/perl
# add_user.pl
#

$| = 1;
$exit_status=0;

#$working_dir = "/u/probe/u1/schemers/src/ADDUSER"; # "/etc";
#$working_dir = "/etc";
#@user_dirs = ("/u/probe/u1/schemers/src/ADDUSER/u");

$working_dir = "/etc";
@user_dirs = ("/u/vela/u1","/u/vela/u2","/u/vela/u3","/u/vela/u4");
$def_group = "students";
$def_gid   = 89;
$def_shell = "/bin/csh";
$def_quota = 4000;

$max_uid = 32000;

chdir($working_dir) || die "can't change to $working_dir\n";

# parse command options

while ( $_ = $ARGV[0]) {
	shift;
	last if /^--$/;
	if    (/^-d/)		{ $nu_parent = &get_option("-dir");	} 
        elsif (/^-f/)  		{ $nu_full   = &get_option("-full");    }
        elsif (/^-g/)  		{ $nu_group  = &get_option("-full");    }
	elsif (/^-h/)		{ &do_help; }
        elsif (/^-p/)  		{ $nu_passwd  = &get_option("-pass");    }
        elsif (/^-q/)  		{ $nu_quota  = &get_option("-quota");    }
        elsif (/^-s/)  		{ $nu_shell  = &get_option("-shell");    }
	elsif (/^[a-zA-Z]*/)    { 
	  &cleanup("can't specify more then one user!",7) if ($nu_user ne "");
	  $nu_user = $_;
	}
	else 		        { &usage("unknown argument: $_");          }
	
}

&usage("must specify one user") if ( $nu_user eq "" );

&usage("must specify a passwd") if ( $nu_passwd eq "");

if ("$nu_full" eq "")   { $nu_full = "$nu_user"; }
if ("$nu_parent" eq "") {
    $lowest_links=32768;
    foreach (@user_dirs) {
        $links = (stat($_))[3];
        if ($links < $lowest_links) { $lowest_links=$links; $nu_parent=$_; }
    }
}

&cleanup("$nu_parent directory not found!",5) if (! -d $nu_parent);

if ($nu_group eq "")   { 
	$nu_group = "$def_group"; 
	$nu_gid   = $def_gid;
} else {
	($t,$t,$nu_gid) = getgrnam($nu_group);
	&cleanup("unknown group: $nu_group",4) if ($nu_gid eq '');
}

if ($nu_shell eq "")   { $nu_shell = "$def_shell"; }
if ($nu_quota eq "")   { $nu_quota = "$def_quota"; }

&catch_signals;
&passwd_lock || &cleanup("couldn't lock passwd file!",1);

($name)=getpwnam($nu_user);

&cleanup("user $nu_user already in passwd file.",3) if ($name ne "");

print "adduser: Adding $nu_user, quota=$nu_quota group=$nu_group\n";

$nu_uid = &next_uid;

if ($nu_uid eq "" || $nu_uid<100 || $nu_uid> $max_uid) {
  &cleanup("next uid error. uid $nu_uid is invalid",6);
}

$nu_home_dir = "$nu_parent/$nu_user";

$nu_encrypted_passwd = &encrypt_passwd($nu_user,$nu_passwd);


(
 open(PASSWD,">>passwd") && 
 (print PASSWD "$nu_user:$nu_encrypted_passwd:$nu_uid:$nu_gid:$nu_full:$nu_home_dir:$nu_shell\n") &&
 close(PASSWD)
) || &cleanup("error creating new passwd file!",10);


if ( -f passwd.dir && -f passwd.pag ) {
     dbmopen(%DBM_PASSWD,"passwd",0644);
     $buf = &pack_passwd_dbm($nu_user,$nu_encrypted_passwd,$nu_uid,$nu_gid,
             $nu_full,$nu_home_dir,$nu_shell);
     $DBM_PASSWD{$nu_user}=$buf;
     $DBM_PASSWD{pack("i",$nu_uid)}=$buf;
     dbmclose(%DBM_PASSWD);
}

&cancel_passwd_lock;

#set the quota here!
if (!&set_new_quota($nu_uid,$nu_parent,$nu_quota)) {
   print "adduser: warning: error setting quota!\n";
}


( 
  mkdir("$nu_home_dir",0711) &&   
  chown($nu_uid,$nu_gid,"$nu_home_dir")
) || &cleanup("error creating home directory $nu_home_dir",9);

chdir("$nu_home_dir") || &cleanup("error changing to directory $nu_home_dir",9);

(
 mkdir("bin",0711) && 
 chown($nu_uid,$nu_gid,"bin")
) || &cleanup("error creating bin directory $nu_home_dir/bin",9);

#( 
#  open(FORWARD,">.forward") &&
#  chmod(0711,".forward") &&
#  chown($nu_uid,$nu_gid,".forward") &&
#  (print FORWARD "$nu_user@vela.acs.oakland.edu") &&
#  close(FORWARD)
#) || &cleanup("error creating .forward $nu_home_dir/.forward",9);

system "cp /usr/skel/.profile /usr/skel/.cshrc /usr/skel/.login .";
chmod(0711,".profile",".cshrc",".login");
chown($nu_uid,$nu_gid,".profile",".cshrc",".login");
chmod(0755,".forward");

chdir($working_dir) || die "can't change to $working_dir\n";

exit 0;


sub usage {
  local($mess) = @_;

  print "adduser: $mess\n\n";

  print <<"_EOF_";
usage:  add_user [-options ...] username

where options include:                                    Defaults -
    -dir      dir             parent directory            least used disk
    -full     "full name"     full name of new user       username
    -group    group           group of new user           $def_group
    -password password        password of new user        username
    -quota    quota           disk quota in kbytes        $def_quota
    -shell    login shell     login shell of new user     $def_shell

All options can be abbreviated up to one letter.

Possible exit codes:
           0 - normal, success           1 - password file is busy
           2 - interrupted               3 - user already in passwd file
           4 - bad group specified       5 - bad parent directory
           6 - error getting new uid     7 - bad arguements (usage)
           8 - error from remote system  9 - error creating user files
          10 - error creating new passwd
_EOF_

 exit 7;
}

sub get_option {
	&usage("missing argument for $_[0]") if ($#ARGV==-1) ;
	$result = $ARGV[0];
	shift @ARGV;	 
	return $result;
}

sub cancel_passwd_lock {
   if (!$passwd_file_locked) { 
	return 0;
   }
   else {
	unlink 'ptmp';
	$passwd_file_locked=0;
	return 1;
   }
}

sub finish_passwd_lock {
   if (!$passwd_file_locked) { 
	return 0;
   } else {
	close(PASSWD);
	close(PTMP);
        chmod 0644,'ptmp';
        rename('passwd','passwd.old');
        rename('ptmp','passwd') || die "can't install new passwd file: $!\n";
	$passwd_file_locked=0;
   } 
}

sub passwd_lock {
  local($retry)=0;

  $the_ptmp = "ptmp.$$";

  open(PTMP,">$the_ptmp") || die"can't create tmp passwd file: $the_ptmp\n";
  close(PTMP);

  if (!link("$the_ptmp",'ptmp') ) {
     print "passwd file busy.";
     while (!link("$the_ptmp",'ptmp')) {
	   if ($retry++ == 24) {
               printf "giving up!\n";
	       unlink("$the_ptmp");
               $passwd_file_locked=0;
	       return 0;
           }
	   sleep(5);
   	   print ".";
     }
     printf "locked!\n";
  }

  $passwd_file_locked=1;
  unlink("$the_ptmp");
  open(PTMP,">ptmp") || die "can't copy passwd file\n";
  open(PASSWD,"passwd") || die "can't open passwd file\n";
  return 1;	

}

sub encrypt_passwd {
  local($user,$pass)=@_;
  local($nslat,$week,$now,$pert1,$pert2);
  local(@salt_set)=('a'..'z','A'..'Z','0'..'9','.','/');
  $now=time;
  ($pert1,$per2) = unpack("C2",$user);
  $week = $now / (60*60*24*7) + $pert1 + $pert2;
  $nsalt = $salt_set[$week % 64] . $salt_set[$now %64];
  return crypt($pass,$nsalt);
}

sub next_uid {
	local(*FILE);
	open (FILE,'tail -1 /etc/passwd|') ||
		die "Can't get last used uid: $?";
	local($name,$pass,$uid)=split(':',<FILE>);
        close(FILE);
        return $uid+1;
}

sub catch_signals {
  $SIG{'INT'} = 'SIGNAL_CLEANUP';
  $SIG{'HUP'} = 'SIGNAL_CLEANUP';
  $SIG{'QUIT'} = 'SIGNAL_CLEANUP';
  $SIG{'PIPE'} = 'SIGNAL_CLEANUP';
  $SIG{'ALRM'} = 'SIGNAL_CLEANUP';
}

sub cleanup {
  local($message,$exit_status) = @_;
  &cancel_passwd_lock;
  unlink("$the_ptmp") if (defined ($the_ptmp));
  print "adduser: $message\n";
  exit $exit_status;
}

sub SIGNAL_CLEANUP {
  &cancel_passwd_lock;
  unlink("$the_ptmp") if (defined ($the_ptmp));
  print "\nadduser: interrupted!\n";
  exit 2;
}

sub unpack_passwd_dbm {
local($buf) = $_[0];
local($i,$l,$name,$passwd,$uid,$gid,$quota,$comment,$gecos,$dir,$shell);

$name	= substr($buf,$i,$l=index($buf,"\0",$i));       $i += $l+1;
$passwd	= substr($buf,$i,$l=index($buf,"\0",$i)-$i);  	$i += $l+1;
($uid,$gid,$quota)=unpack("i i i",substr($buf,$i,12));	$i += 12;
$comment= substr($buf,$i,$l=index($buf,"\0",$i)-$i);	$i += $l+1;
$gecos	= substr($buf,$i,$l=index($buf,"\0",$i)-$i);	$i += $l+1;
$dir	= substr($buf,$i,$l=index($buf,"\0",$i)-$i);	$i += $l+1;
$shell	= substr($buf,$i,$l=index($buf,"\0",$i)-$i);	$i += $l+1;
return ($name,$passwd,$uid,$gid,$gecos,$dir,$shell);
}


sub pack_passwd_dbm {

local($name,$passwd,$uid,$gid,$gecos,$dir,$shell) = @_;
local($i,$l,$quota,$comment,$buf);

$buf = $name . "\0" . $passwd . "\0" . pack("iii",$uid,$gid,0)  .
       "\0" . $gecos . "\0" . $dir . "\0" . $shell . "\0";

return $buf;
}

sub set_new_quota { #user,dir,bs
  local ($SYS_quota)=149;
  local ($Q_SETDLIM)=1;
  local ($uid,$dir,$bs) = @_;
  local ($dev)   = stat($dir);
  local ($dqblk) = pack("LLLSSSCC",($bs+1000)*2,($bs*2),0,0,0,0,3,3);
  local ($stat,$buf);
  $stat=syscall($SYS_quota,$Q_SETDLIM,$uid,$dev,$dqblk);
  return $stat==0;
}


Roland J. Schemers III              |            Networking Systems
Systems Programmer                  |            168 Pine Hall   (415)-723-6740
Distributed Computing Group         |            Stanford, CA 94305-4122
Stanford University                 |            schemers@jessica.Stanford.EDU


From tchrist@convex.COM Tue Mar 10 23:25:16 1992
To: Perl-Users@fuggles.acc.Virginia.EDU
From: Tom Christiansen <tchrist@convex.COM>
Subject: Re: add_user.pl
Reply-To: tchrist@convex.COM (Tom Christiansen)
Date: Wed, 26 Feb 1992 19:19:39 GMT
SUB: Re: add_user.pl
SUM: Tom Christiansen <tchrist@convex.COM>, tchrist@convex.COM (Tom Christiansen)->Perl-Users@fuggles.acc.Virginia.EDU

>From the keyboard of schemers@jessica.stanford.edu (Roland Schemers):
: o  it adds the quota by calling the quota system call from perl
:
:sub set_new_quota { #user,dir,bs
:  local ($SYS_quota)=149;
:  local ($Q_SETDLIM)=1;
:  local ($uid,$dir,$bs) = @_;
:  local ($dev)   = stat($dir);
:  local ($dqblk) = pack("LLLSSSCC",($bs+1000)*2,($bs*2),0,0,0,0,3,3);
:  local ($stat,$buf);
:  $stat=syscall($SYS_quota,$Q_SETDLIM,$uid,$dev,$dqblk);
:  return $stat==0;
:}

Unfortunately, this kind of code is apt to mysteriously fail because you
are just guessing what all these constants are on all systems.  They may
work on your system, but they don't on mine.  For example, there's no
SYS_quota anymore, and I don't even know whether the 149 entry point to it
still works.    

It would appear that these days systems use quotactl instead of quota. 
Here's what I might do, although maybe I'd protect the ufs/quota.ph
require, since I don't know what the real include is for systems with
SYS_quota.

    require 'sys/syscall.ph';
    require 'ufs/quota.ph';

    if (defined(&SYS_quotactl) && defined(&Q_SETQUOTA)) {
	# use SYS_quotactl
    } elsif (defined(&SYS_quota) && defined(&Q_SETDLIM)) {
	# do orig code
    } else {
	die 'snafu';
    } 

I don't know whether the args to the old quota look like quotactl. 
On my system, quotactl should be called this way:

     quotactl(cmd, special, uid, addr)
	 int cmd; 
	 char *special; 
	 int uid; 
	 caddr_t addr;


Which means that I think the call should look like this with the 
uid and dev switched around.    

    syscall(&SYS_quotactl, &Q_SETQUOTA, $dev, $uid, $dqblk);

I'm not sure what to do there because I haven't use such a system.
But at least by using &Q_SETDLIM you'll get a fatal if there's
a problem.

The other problem is that my struct dqblk isn't a "LLLSSSCC": it's all
unsigned longs.  If you use c2ph on your ufs/quota.h file, you can use the
&dqblk'blah definitions to do this in a machine-indepdent way:

    @S = ();
    $S[&dqblk'dqb_bhardlimit] = ($bs+1000)*2;
    $S[&dqblk'dqb_bsoftlimit] = $bs * 2;
    $S[&dqblk'dqb_btimelimit] = 3;
    $S[&dqblk'dqb_ftimelimit] = 3;
    $dqblk  = pack(&dqblk'typedef(), @S);


Now you don't have to know what the types are or their layout.

This should help folks who'd like to use this code but whose 
systems are different from the original posters.

--tom


