#!/root/usr/sbin/perl #Tag 0x00000f00 #ident "$Revision $" # # this is loaded from the miniroot, and the Perl binary only resides # in the mounted "real" filesystem. # regenerate HTML by (requires Perl v5.003 or later): # pod2html --infile=ConfigMR.pl --outfile=ConfigMR.html =head1 NAME ConfigMR - utility functions for roboinst post-miniroot-inst =head1 DESCRIPTION This Perl library file is designed to supply some standard usable functions for system configuration in the miniroot at the end of a robo-inst. This can't use standard Perl module form, because the Perl standard library might not be available (eg: should the system be configured to NFS mount F, the Perl library resides under F, and NFS doesn't work in the miniroot). All pathnames referenced in this document are those as seen in the miniroot, unless specified otherwise. =head1 COPYRIGHT This software is Copyright Silicon Graphics Inc, 1997,1998. Feel free to use and enhance for your own needs, but keep this Copyright notice. =head1 GLOBAL VARIABLES A few variables are made global for customization, though they are mostly useful for debugging and informational purposes. =over 4 =item B<$VERSION> The RCS version number of this library. =cut BEGIN { # set the version for version checking $VERSION = do { my @r = (q$Revision: 1.2 $ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r }; # must be all one line, for MakeMaker } =item B<$mountdir> The mount point of the real filesystem under the miniroot. This is F to match what the miniroot does. It is accessible so that it can be changed for testing the script. A common value for testing might be F (be B careful!) or F. =cut $mountdir = $ENV{"SGI_ROOT"}; # value as used in miniroot. # global so that it can be overridden =item B<$custom> The directory into which the custom configuration files are copied. This is F in the miniroot filesystem. For testing, this can be an arbitrary path (eg: F<$HOME/project/test/custom.3>). =cut $custom = $ENV{"SGI_CUSTOM"}; # value as used in miniroot =item B<$custuser> The directory (under F) under which the user customization scripts are stored. It must be skipped when installing system customization files. When installing user files, files are first looked for under F<$custom/$custuser/$user/>. If that directory doesn't exist, then the files under F<$custom/$custuser/default/> are copied to the new user's account. The default value for this directory is F<=passwd=>. =cut $custuser = "=passwd="; =item B<@backupext> When configuration files supplied in the "/custom" dir would overwrite an installed file, the installed file will be renamed with a backup extension. The defaults of I<.O> and I<.N> mean that the files will be listed from the I command. I<.ORIG> handles the case that inst has used both. =cut @backupext = (".O", ".N", ".ORIG"); =back =head1 CALLER SCRIPT CAVEATS Since Perl does not exist in the miniroot, scripts written to use the functions in this library file must be coded in a special way due to: =over 4 =item * The Perl binary in the F filesystem must be used. =item * The compiled in library path (I<@INC>) is incorrect, so it must be overridden. =item * The Perl standard library is sharable, so may not be installed in the F filesystem, and NFS does not work in the miniroot. So this library is written to not require the existence of the library (hence it is not a module, and some parts are coded oddly). =back To handle these issues, roboinst or miniroot Perl scripts need to use the following prologue: #!/root/usr/bin/perl # # Custom configuration script for use in the miniroot # # Fix up the Perl @INC for use in the miniroot: # (assumes the version originally shipped with IRIX # adjust accordingly if Perl is patched or updated) BEGIN { require 5.003; # min version; we're shipping 5.004+... # this method works on all IRIX/Perl5 releases my(@new) = reverse @INC; my($dir); foreach $dir (@new) { unshift(@INC, "/root".$dir); } unshift(@INC, "/custom"); # chicken-or-egg: $custom not yet defined } # by using unshift() instead of assignment, the standard library # locations are still there for debugging. # NOTE: because the Perl library is in /usr/share, and the system may be # configured to NFS mount /usr/share, and NFS mounts won't be active in # the miniroot, it would be wise to not use any standard modules in the # miniroot, unless you are sure that they will be there... require "ConfigMR.pl"; # for testing only: $debug = @ARGV ? 1 : 0; $mountdir = shift if @ARGV; $custom = shift if @ARGV; =head1 FUNCTIONS The following functions are exported as part of this module. They are defined in this order to avoid forward references. =over 4 =item B Do a I<`chroot $mountdir @cmd`>, checking that the environment is "safe". Mostly just checks that an F<_RLD*ROOT> environment variable, if it exists, contains F, otherwise the Fed command will fail. The directory F is the directory under which the normal filesystems is mounted. =cut sub chrootcmd { my(@cmd) = @_; my($ev); foreach $ev (qw(_RLDN32_ROOT _RLD64_ROOT _RLD_ROOT)) { if ($ENV{$ev}) { if (! grep(/^\/$/, split(/:/, $ENV{$ev}))) { $ENV{$ev} = "/:" . $ENV{$ev}; } } } system( "/etc/chroot", $mountdir, @cmd); } =item B This is a convenience interface to the I command on the F filesystem. It takes the normal arguments (plus I, which does not change the value), and returns the value B or B<"">. It returns B if either the $item or the argument is invalid or non-existent. The optional I argument is needed to create a new chkconfig item. =cut sub chkconfig { my($item, $value, $force) = @_; if ($value eq "on" or $value eq "off") { if ($force) { chrootcmd("/etc/chkconfig", "-f", $item, $value); } else { chrootcmd("/etc/chkconfig", $item, $value); } } elsif ($value ne 'query') { return undef; } my($file) = "$mountdir/etc/config/$item"; open(CHK, "<$file") or return undef; my($state) = ; close(CHK); return $state eq "on" ? $state : ""; } =item B The mounted real filesystem could have absolute symlinks which, if followed, will lead to the wrong place in the filesystem. This function maps them to a valid real path from the miniroot F. Returns the "fixed" path, or undef if there's a problem. =cut sub realpath { my($logpath) = @_; my($head) = ""; if (substr($logpath,0,length($mountdir)) eq $mountdir) { $logpath = substr($logpath,length($mountdir)); $head = $mountdir; } my(@bits) = split(/\//, $logpath); shift(@bits) unless $bits[0]; # get rid of leading empty component my($fixed) = "/"; my($linkv); while (@bits) { while ( -l $mountdir.$fixed) { $linkv = readlink ($mountdir.$fixed); if (substr($linkv,0,1) eq "/") { # absolute symlink replaces current one $fixed = $linkv; } else { last; } } $fixed .= "/" . shift(@bits); }; return $head.$fixed; } =item B Copy the password file from F. If this file contains a user named I, then it replaces the standard one in F, else it is appended to that file. The original is saved as F. If the argument is true, then it makes a shadow password file. =cut sub cpPasswd { my($mkshadow) = @_; my($src, $dest) = ($custom."/passwd", $mountdir."/etc/passwd"); if (-s $src) { my($bux); open(CPW, "<$src") or do { warn "$src exists but can't read? $!\n"; return undef; }; my(@lines) = ; close(CPW); my($tdir) = substr($dest,0,rindex($dest,"/")); system("/bin/mkdir", "-p", $tdir) unless -d $tdir; if (! grep(/^root/, @lines)) { # if appending, copy the saved original, first foreach $bux (@backupext) { next if ( -f "$dest$bux"); open(DBX, "<$dest$bux") and do { print OUT ; close(DBX); }; last; } open(OUT, ">>$dest") or do { warn "can't open $dest for append? $!\n"; return undef; }; } else { # if replacing, rename the original first foreach $bux (@backupext) { next if ( -f $dest.$bux); rename($dest, $dest.$bux) if -s $dest; last; } open(OUT, ">$dest") or do { warn "can't open $dest for write? $!\n"; return undef; }; } print OUT @lines; close(OUT); chmod(0444, $dest); } if ($mkshadow) { chrootcmd("/sbin/pwconv"); } } =item B Add an entry to the F file. If the $hostnames[0] already exists but different, the existing one is commented out, and the new one replaces it. If it doesn't exist, it is added to the end of the file. The value as added is returned. If either the $ip or the @hostnames is empty, only a lookup is performed. The return value is a string of the entry as found or added. This reads the file each time. For bulk additions, it is better to copy a complete hosts file (eg: using I). =cut sub add2hosts { my($ip, @hosts) = @_; my($hostsfile) = "$mountdir/etc/hosts"; open(A2H, "<$hostsfile") or do { warn "can't read $hostsfile? $!\n"; return undef; }; my(@lines) = ; close(A2H); my(@rec) = split(' ', grep(/\b$hosts[0]\b/, @lines)); my($update); if ($ip and @hosts) { # potential add/update my($match) = $ip eq $rec[0] ? 1 : 0; my($i); for ($i = 0; $match && $i <= $#hosts; $i++) { $match = grep(/^$hosts[$i]$/, @rec); } if (! $match) { my($temp) = $hostsfile.".NEW"; $update = $ip . "\t" . join(" ", @hosts); open(OUT, ">$temp") or do { warn "can't write to $temp? $!\n"; return undef; }; for ($i = 0; $i <= $#lines; $i++) { if ($lines[$i] =~ /^$hosts[0]$/) { print OUT $update,"\n"; print OUT "#" . $lines[$i]; $update = ""; } else { print OUT $lines[$i]; } } if ($update) { print OUT $update,"\n"; } close(OUT); my($bak) = $hostsfile . ".ORIG"; rename($hostsfile, $bak) unless -s $bak; # only first time rename($temp, $hostsfile); } } else { # query only $update = shift(@rec) . "\t@rec"; } return $update; } =item B Merge the contents of F<$file> (assumed to be in hosts file format) with F using the rules of I (above). It returns the number of lines merged. Note that comments in F<$file> are ignored, and do not make it into F. =cut sub mergeHostFile { my($file) = @_; my($count) = 0; if ( -f $file ) { open(FILE, "<$file") or do { warn "can't open $file for read? $!\n"; return undef; }; my($ip, @names); while () { next if /^\#/; chomp; ($ip, @names) = split; $count++ if add2hosts($ip, @names); } close(FILE); } else { $count = 0; } return $count; } =item B This convenience function just forces values for the minimum networking files: F, F, and F. If $sys_id does not exist in F, then it is added. All arguments have default values. $sys_id is `hostname`, $ypdomain and $ip are fetched from F (making it the same as the DNS domain), and $netmask is 0xffffff00. If IP or hostname are not found, then returns I, else returns 1. =cut sub configNetworking { my($id, $domain, $netmask, $ip) = @_; unless ($id) { $id = `hostname`; chomp($id); } unless ($netmask) { $netmask = '0xffffff00'; } unless ($domain) { my(@rec); open(HF, "))[0]); chomp(@rec); close(HF); my(@t) = split(/\./,$rec[1]); shift(@t); $domain = join('.',@t); unless ($ip) { $ip = $rec[0]; } } my($sys_id) = "$mountdir/etc/sys_id"; my($tdir) = substr($sys_id,0,rindex($sys_id,"/")); system("/bin/mkdir", "-p", $tdir) unless -d $tdir; open(OUT, ">$sys_id") or do { warn "can't write to $sys_id? $!\n"; return undef}; print OUT "$id\n"; close(OUT); if ($domain) { # don't bother unless there is a value for $domain my($ypdomain) = "$mountdir/var/yp/ypdomain"; $tdir = realpath(substr($ypdomain,0,rindex($ypdomain,"/"))); system("/bin/mkdir", "-p", $tdir) unless -d $tdir; open(OUT, ">$ypdomain") or do { warn "can't write to $ypdomain? $!\n"; return undef; }; print OUT "$domain\n"; close(OUT); } my($ifconfig) = "$mountdir/etc/config/ifconfig-1.options"; $tdir = substr($ifconfig,0,rindex($ifconfig,"/")); system("/bin/mkdir", "-p", $tdir) unless -d $tdir; open(OUT, ">$ifconfig") or do { warn "can't write to $ifconfig? $!\n"; return undef; }; print OUT "netmask $netmask\n"; close(OUT); my(@hosts); if ($id =~ /([-\w]+)\./) { push(@hosts, $id); my($bare) = $1; if ($id !~ /$domain/) { push(@hosts, $bare.".".$domain); } push(@hosts, $bare); } else { push(@hosts, $id.".".$domain, $id); } my(@hostent) = add2hosts($ip, @hosts); return @hostent ? 1 : undef; } =item B Installs a directory tree of files (usually configuration files) from the F directory into the F filesystem. It will create missing directories, and save existing files aside by appending F<.ORIG> to the name. The optional argument(s) are directories to copy. By default it copies all directories below (but not including) the F directory. Note that the F and F file are handled specially by separate functions. It is usually more convenient to create user directories with the supplied special functions. To support them, the special dir F is skipped. =cut sub installTree { my(@dirs) = @_; my($copied) = 0; if (! @dirs) { opendir(DIR, $custom) or do { warn "can't opendir($custom)? $!\n"; return undef; }; @dirs = grep { /^[^\.]/ && -d "$custom/$_" } readdir(DIR); closedir(DIR); } @dirs = grep(!/^$custuser$/, @dirs); $copied = cp_rp($custom, $mountdir, @dirs); return $copied; } # multi-purpose sub sub cp_rp { my($fromdir, $todir, @dirs) = @_; my($copied) = 0; my($basewd) = `/bin/pwd`; if (@dirs) { chdir($fromdir); open(FILES, "/sbin/find @dirs -print |") or do { warn "can't open find? !$\n"; return undef; }; my($src, $dest); while() { chomp; $src = $fromdir."/".$_; $dest = realpath($todir."/".$_); if (-d $src) { system("/bin/mkdir", "-p", $dest) unless -d $dest; } else { if (-s $dest) { my($bu); foreach $bu (@backupext) { unless (-f $dest.$bu) { rename($dest, $dest.$bu); } } } system("/bin/cp", "-p", $src, $dest); $copied++; } } close(FILES); chdir($basewd); } return $copied; } =item B Similar to the I function, but this one stores the files in a "flattened" manner, with the directory separator (F) replaced by the F<$sep> character, F<%> by default. Ie: the file F would be installed as the file F. =cut sub installSep { my($sep) = @_; $sep = "%" unless $sep; my($copied) = 0; opendir(DIR, $custom) or do { warn "can't opendir($custom)? $!\n"; return undef; }; my(@files) = grep { /^$sep/ && -f $custom."/".$_ } readdir(DIR); closedir(DIR); my($src, $dest); foreach $src (@files) { ($dest = $src) =~ s,$sep,/,g; $dest = $mountdir. realpath("/".$dest); $dest =~ s,//,/,g; my($tdir) = substr($dest, 0, rindex($dest,"/")); if (! -d $tdir) { system("/bin/mkdir", "-p", $tdir); } if (-s $dest) { rename($dest, $dest.".ORIG"); } system("/bin/cp", $custom."/".$src, $dest); $copied++; } return $copied; } =item B This is a home-grown version of I that will return something reasonable from F even in the presence of NIS entries, as they would probably choke the system, since NIS isn't available in the miniroot. Return values are the same as the standard Perl I function. =cut sub getpwname { my($name) = @_; my($passwd) = "$mountdir/etc/passwd"; open(GPW, "<$passwd") or do { warn "can't read $passwd? $!\n"; return undef; }; my($line) = grep(/^\+?$name:/, ); close(GPW); if ($line) { chomp($line); my(@f) = split(/:/, $line); if ($f[0] =~ /^\+(.*)/) { $f[0] = $1; } return (@f[0,1,2,3],"","",@f[4,5,6]); } else { return undef; } } =item X> Create the user account if it doesn't exist: get the info from F, then create the home directory and populate it. Note that for NIS passwords, all but the username field is optional. In this case, no home directory can be created since NIS is not available in the miniroot. Add the default user configuration files: Look first for F, and if not found, look for F, where F (as a directory tree) are what is placed in the user's home directory. If none of these are found, will use the F files to create the user's environment. The optional argument $mode will force the permissions on the user's home directory. If not specified, it defaults to I<01755>. Modes on the copied files will match those installed into F. Normally, this function will not do anything if the user's home directory allready exists. Setting the optional I argument will copy the files anyway. =cut %uadded=(); sub makeauser { my($username, $mode, $force) = @_; return 1 if defined $uadded{$username}; # # create the directory if it doesn't exist # my($uid, $gid, $home) = (getpwname($username))[2,3,7]; return undef unless ($uid && $gid && $home); my($rhome) = $mountdir . realpath($home); $mode = "01755" unless $mode; if (! -d $rhome) { system("/bin/mkdir", "-p", $rhome); chmod(oct($mode), $rhome); chown($uid, $gid, $rhome); } elsif (! $force) { return 0; } # # copy user's configurations. # try $username first, then "user". Note, using shell wildcards # my($customuser) = ("$custom/$custuser"); my($copied, @files); my($fromdir) = ("$customuser/$username"); if (! -d $fromdir) { $fromdir = "$customuser/default"; } if ( -d $fromdir ) { opendir(DIR, $fromdir) or do { warn "can't opendir($fromdir)? $!\n"; return undef; }; @files = grep { !/^\.\.?$/ } readdir(DIR); closedir(DIR); $copied = cp_rp($fromdir, $rhome, @files); } # copy standard .rc files if missing if (-f "$mountdir/etc/stdcshrc" && ! -f "$rhome/.cshrc") { system("/sbin/cp","$mountdir/etc/stdcshrc","$rhome/.cshrc"); } if (-f "$mountdir/etc/stdlogin" && ! -f "$rhome/.login") { system("/sbin/cp","$mountdir/etc/stdlogin","$rhome/.login"); } if (-f "$mountdir/etc/stdprofile" && ! -f "$rhome/.profile") { system("/sbin/cp","$mountdir/etc/stdprofile","$rhome/.profile"); } system ("/sbin/chown", "-R", "$uid.$gid", $rhome); $uadded{$username} = 1; return $copied; } =item B Create all of the local users from the F file, if their home directory does not exist. This routine just loops over F, calling L on each one. =cut sub makeusers { my($mode) = @_; my($copied); my($pwdfile) = $mountdir."/etc/passwd"; open(GPD, "<$pwdfile") or do { warn "hey, where's $pwdfile? $!\n"; return undef; }; while() { my($name, $pas, $uid, $gid, $gcos, $home, $shell) = split(/:/, $_); my($rhome) = $mountdir . $home; if ($home and $home ne "/" and $home ne "/dev/null" and $uid > 100 and ! -d $rhome) { $name =~ s/^\+//; makeauser($name, $mode) && $copied++; } } close(GPD); return $copied; } =item B This function will move directory trees around the filesystems. $src and $dest are paths relative to the normal filesystem. The default is to leave a symlink pointing from the old location to the new location, the optional $nolink argument suppresses this. This function correctly handles moves within and across mounted filesystems. The destination directory may exist, but the filesystem must be mounted first. Example 1: move the F directory to F, without leaving a symlink behind: movedir "/usr/people", "/home", 1; Example 2: move F to F, and leave a symlink: movedir "/opt", "/usr/share/opt"; Returns I<0> on success and I on failure. =cut sub movedir { my($src, $dest, $nolink) = @_; my($from) = $mountdir . realpath($src); my($to) = $mountdir . realpath($dest); my($fdev,$fino,$fmode) = stat($from); return $! unless ($fdev); # srcdir doesn't exist my($limb) = $to; while (! -d $limb) { $limb =~ s,/[^/]*$,,; } my($tdev,$tino,$tmode) = stat($limb); if (! -d $to) { system("/sbin/mkdir", "-p", $to); return $! unless -d $to; } if ($tdev == $fdev) { opendir(D, $from) or return $!; my($d); while (defined($d = readdir(D))) { next if ($d eq "." or $d eq ".."); rename($from."/".$d, $to."/".$d) or warn "rename $from/$d to $to/$d? $!\n"; } close(D); rmdir($from) or return $!; } else { system("/bin/find $from/. -depth -print | /bin/cpio -pdm $to"); system("/bin/rm", "-rf", $from); } unless ($nolink) { symlink($dest, $from) or return $!; } return 0; } =item B Make this machine an NDS (Network Dual-head Software) slave. The arguments are the F<$DISPLAY> of the NDS master, and options to the F daemon as specified in the F file. As side-effects, F is I'ed on, and it is started from the F file. I returns undef if the NDS configuration files are not installed, 0 otherwise. =cut sub mkNDSslave { my($master,@options) = @_; # # update the dh_config file # my($dh_config) = "$mountdir/usr/nds/dh_config"; my(@dh_mode) = stat($dh_config); my($temp) = "$mountdir/usr/nds/dh_backup"; # # not acceptable to die() in the miniroot, so warn() and return; # open(DHIN, "<$dh_config") or do { warn "can't open $dh_config for read? $!\n"; return undef; }; open(TOUT, ">$temp") or do { warn "can't open $temp for write? $!\n"; close(DHIN); return undef; }; while () { last unless /^#/; print TOUT; } $master .= ":0.0" unless $master =~ /:/; print TOUT "-master $master\n"; my($f); foreach $f (@options) { print TOUT "$f\n"; } close(DHIN); close(TOUT); chown(@dh_mode[4,5], $temp); chmod($dh_mode[2], $temp); rename($dh_config, "$dh_config.O"); rename($temp, $dh_config); # # configure the Xstartup file for startup # my($xstartup) = "$mountdir/var/X11/xdm/Xstartup"; open(XSIN, "<$xstartup") or do { warn "can't open $xstartup for read? $!\n"; return undef; }; my(@lines) = ; close(XSIN); my($match) = grep(/nds\s+start/, @lines); unless ($match) { $temp = "$mountdir/var/X11/xdm/temp$$"; my(@Xmode) = stat($xstartup); open(XOUT, ">$temp") or do { warn "can't open $temp for write? $!\n"; return undef; }; my($i,$n) = (0, $#lines); while ($lines[$n] =~ /^\s*$/) { $n--; } while ($lines[$n] =~ /^\#|^exit/) { $n--; } for ($i=0; $i <= $n; $i++) { print XOUT $lines[$i]; } print XOUT "#\n# enable NetworkDualhead\n"; print XOUT "/etc/init.d/nds start silent\n\n"; for (; $i <= $#lines; $i++) { print XOUT $lines[$i]; } close(XOUT); chown(@Xmode[4,5], $temp); chmod($Xmode[2], $temp); rename($xstartup, "$xstartup.O"); rename($temp, $xstartup); } # # enable the NDS to start next boot # chrootcmd("/etc/chkconfig", "nds", "on"); return 0; } 1; __END__ =back =head1 AUTHOR Scott Henry =head1 BUGS The password appending function should arguably do a merge on username. =cut # emacs fodder # # Local Variables: # mode: perl # indent-tabs-mode: t # wrap-column: 76 # End: #