#!/usr/bin/perl -w # $Id: rcs.mgr,v 3.6 2003/04/21 18:09:42 rowan Exp $ # Manage workstation configuration files in an RCS repository # Rowan Littell use strict; use Fcntl qw (:DEFAULT :flock); use IO::Handle; use Getopt::Long; use POSIX qw(waitpid); use Sys::Syslog qw (:DEFAULT setlogsock); # This is the RCS revision number my $VERSION = (split /\s+/, '$Revision: 3.6 $')[1]; my $STATE = (split /\s+/, '$State: Exp $')[1]; # add another path for where your RCS commands live # /usr/bin is common on free OSen # /opt/TWWfsw/bin is for systems like Solaris using The Written Word packages my @RCS_PATH = qw( /usr/bin /usr/local/bin /opt/TWWfsw/bin ); my @GEN_PATH = qw( /bin /usr/bin /sbin /usr/sbin /usr/local/bin ); # set up locations for necessary commands my $RCS = FindFirstExec ("rcs", @RCS_PATH); my $RCS_CO = FindFirstExec ("co", @RCS_PATH); my $RCS_CI = FindFirstExec ("ci", @RCS_PATH); my $RLOG = FindFirstExec ("rlog", @RCS_PATH); my $INSTALL = FindFirstExec ("install", @GEN_PATH); my $UNAME = FindFirstExec ("uname", @GEN_PATH); my $MD5 = FindFirstExec ("md5", @GEN_PATH); my $MD5SUM = FindFirstExec ("md5sum", @GEN_PATH); my $DIFF = FindFirstExec ("diff", @GEN_PATH); my $uname = ""; # default to none # working defaults (used if creating a repository or if config file isn't found) my @CONFIG = qw( /etc/rcsmgr.conf /usr/local/etc/rcsmgr.conf rcsmgr.conf ); my $working_dir = ""; my $rcs_path_sep = ':'; my $editor = FindFirstExec ("vi", @GEN_PATH); my $nullmd5 = "d41d8cd98f00b204e9800998ecf8427e"; my $installdb = ""; my %DEFAULT_ATTR = ( "mode" => oct (444), "user" => (getpwuid 0)[0], "group" => (getgrgid 0)[0], "commands" => "", "md5sum" => $nullmd5, "editor" => $editor ); my $silent = 0; MAIN: { my (%opts, $yesno); $yesno = 0; # allow short and long options Getopt::Long::Configure ("bundling"); # get options -- we have short options for historical compatibility # and we have long options for user-friendliness and flexibility GetOptions ( # short option long option 'm=i' => \$opts{'m'}, 'mode=i' => \$opts{'m'}, 'u=s' => \$opts{'u'}, 'user=s' => \$opts{'u'}, 'g=s' => \$opts{'g'}, 'group=s' => \$opts{'g'}, 'I=s' => \$opts{'I'}, 'initialize=s' => \$opts{'I'}, 'L=s' => \$opts{'L'}, 'log|showlog=s' => \$opts{'L'}, 'e=s' => \$opts{'e'}, 'edit=s' => \$opts{'e'}, 'a=s' => \$opts{'a'}, 'add=s' => \$opts{'a'}, 'i=s' => \$opts{'i'}, 'install=s' => \$opts{'i'}, 'r=s' => \$opts{'r'}, 'remove=s' => \$opts{'r'}, 'v=s' => \$opts{'v'}, 'version|revision=s' => \$opts{'v'}, 'M=s' => \$opts{'M'}, 'message=s' => \$opts{'M'}, 'c=s' => \$opts{'c'}, 'config=s' => \$opts{'c'}, 'C=s' => \$opts{'C'}, 'change=s' => \$opts{'C'}, 'd=s' => \$opts{'d'}, 'diff=s' => \$opts{'d'}, # options that only exist in long form 'checkout=s' => \$opts{'checkout'}, 'checkin=s' => \$opts{'checkin'}, 'checkall' => \$opts{'checkall'}, 'check=s' => \$opts{'check'}, 'updateall' => \$opts{'updateall'}, 'update=s' => \$opts{'update'}, 'attributes=s' => \$opts{'attrlist'}, # options that don't take values 's' => \$opts{'s'}, 'silent' => \$opts{'s'}, 'l' => \$opts{'l'}, 'list' => \$opts{'l'}, 'y' => \$opts{'y'}, 'yes|runcommands|defaults' => \$opts{'y'}, 'n' => \$opts{'n'}, 'no' => \$opts{'n'}, 'h' => \$opts{'h'}, 'help' => \$opts{'h'} ); # connect to syslog openlog ('rcs.mgr', 'pid', 'user'); setlogsock ('unix'); # be quiet ($opts{'s'}) && ($silent = 1); # check yesno $yesno = 1 if ($opts{'y'}); $yesno = 0 if ($opts{'n'}); # create the repository and initial config file ($opts{'I'}) && (InitRCSRepository ($opts{'I'})); # use a specified config file ($opts{'c'}) && (unshift @CONFIG, $opts{'c'}); # now we can read the config file, if there is one ReadConfig(); # parse attribute list option ($opts{'attrlist'}) && (%DEFAULT_ATTR = ParseAttrOption ($opts{'attrlist'}, %DEFAULT_ATTR)); # and the deprecated user, group and mode options ($opts{'m'}) && ($DEFAULT_ATTR{"mode"} = oct $opts{'m'}); ($opts{'u'}) && ($DEFAULT_ATTR{"user"} = $opts{'u'}); ($opts{'g'}) && ($DEFAULT_ATTR{"group"} = $opts{'g'}); # give defaults and a brief help message if ($opts{'h'}) { print "You are running on: $uname\n"; print "RCS.MGR version: $VERSION-$STATE\n"; print "Current defaults (actual values may depend on install database):\n"; print " working directory = '$working_dir'\n"; print " user = '".$DEFAULT_ATTR{"user"}."'\n"; print " group = '".$DEFAULT_ATTR{"group"}."'\n"; printf " mode = '%04o'\n", $DEFAULT_ATTR{"mode"}; print " editor = '$editor'\n"; print " path separator = '$rcs_path_sep'\n"; print " install database = '$installdb'\n"; print "For usage information, run 'perldoc $0'\n"; #system "perldoc", $0; closelog(); exit (0); } # we need to be in the working dir for RCS to work right chdir $working_dir; # list managed files ($opts{'l'}) && (RCSIndex ("$working_dir/RCS")); # show log history of a file ($opts{'L'}) && (LogFile ($opts{'L'})); # add a file to the repository ($opts{'a'}) && (AddFile ($opts{'a'}, $yesno, $opts{'M'})); # edit a file ($opts{'e'}) && (EditFile ($opts{'e'}, $opts{'v'}, $opts{'M'}, $yesno)); # remove a file from the repository ($opts{'r'}) && (RemoveFile ($opts{'r'})); # change attributes on a file ($opts{'C'}) && (ChangeAttr ($opts{'C'}, $opts{'attrlist'})); # simple checkout of a file ($opts{'checkout'}) && (CheckInOut ($opts{'checkout'}, "out", "lock", $yesno, $opts{'M'}, $opts{'v'})); # simple checkin of a file ($opts{'checkin'}) && (CheckInOut ($opts{'checkin'}, "in", "", $yesno, $opts{'M'}, $opts{'v'})); # check modifications of a file or all files ($opts{'check'}) && (CheckMD5 ($opts{'check'})); ($opts{'checkall'}) && (CheckMD5 ("*")); ($opts{'update'}) && (UpdateMD5 ($opts{'update'})); ($opts{'updateall'}) && (UpdateMD5 ("*")); # show diffs on a file ($opts{'d'}) && (DiffFile ($opts{'d'}, $opts{'v'})); # install a file ($opts{'i'}) && (InstallFile ($opts{'i'}, $opts{'v'}, $yesno)); } sub FindFirstConfig { my ($conf); foreach $conf (@_) { return $conf if (-e $conf && ! -d $conf); } } sub FindFirstExec { my ($prog, @paths) = @_; my ($p); foreach $p (@paths) { return "$p/$prog" if (-x "$p/$prog" && ! -d "$p/$prog"); } return (""); } # parse attributes command line option # --attributes="user=foo,group=bar,mode=0400,commands=cmd1;cmd2;cmd3;...;cmdn" sub ParseAttrOption { my ($optline, %default) = @_; my (%attrlist, @elems); %attrlist = %default; if (defined $optline) { @elems = split (/,/, $optline, 4); foreach (@elems) { my ($attr, $value) = (split /=/)[0,1]; if (grep /^$attr$/, qw ( mode user group commands editor )) { if ($attr eq "mode") { $attrlist{"$attr"} = oct ($value); } else { $attrlist{"$attr"} = $value; } } } } return (%attrlist); } # read the CONFIG file of a working directory, if it exists sub ReadConfig { my ($rcsfile) = ""; my ($conf) = FindFirstConfig (@CONFIG); my ($sep) = '='; open CONFIG, $conf; while () { chomp; if ($_ !~ /^#/) { (/^WORKING DIRECTORY/i) && (($working_dir) = (split /$sep/, $_)[1]); (/^PATH/i) && ($rcs_path_sep = (split /$sep/, $_)[1]); (/^USER/i) && ($DEFAULT_ATTR{"user"} = (split /$sep/, $_)[1]); (/^GROUP/i) && ($DEFAULT_ATTR{"group"} = (split /$sep/, $_)[1]); (/^MODE/i) && ($DEFAULT_ATTR{"mode"} = oct ((split /$sep/, $_)[1])); (/^EDITOR/i) && ($editor = (split /$sep/, $_)[1]); (/^INSTALLDB/i) && ($installdb = (split /$sep/, $_)[1]); } } close CONFIG; $working_dir =~ s/ //g; $rcs_path_sep =~ s/ //g; $DEFAULT_ATTR{"user"} =~ s/ //g; $DEFAULT_ATTR{"group"} =~ s/ //g; $editor =~ s/ //g; $installdb =~ s/ //g; if ($rcs_path_sep =~ /\//) { die "Error: illegal forward slash (/) found in path separator."; } # let environment override the editor if (defined $ENV{'EDITOR'} && $ENV{'EDITOR'} ne "") { $editor = $ENV{'EDITOR'}; } # get system type for system-dependant parts open (UNAME, "-|") || exec $UNAME, "-sr"; $uname = (); close (UNAME); chomp ($uname); } # given a filename (either RCS or live), generate both RCS and live filenames sub FileCvt { my ($file) = @_; my ($rcsfile) = ""; if ($file =~ /\//) { $rcsfile = $file; $rcsfile =~ s/\//$rcs_path_sep/g; } else { $rcsfile = $file; $file =~ s/$rcs_path_sep/\//g; } if ($file !~ /^\//) { die "Error: specify the full path name."; } return ($file, $rcsfile); } sub RCSCheckOut { my ($rcsfile, $revision, $lock) = @_; my (@args); my ($rv); if (-w $rcsfile) { # file is already checked out for writing ($lock =~ /^lock/i) && (return (0)); # if we're installing, this is an error ($lock =~ /^nolock/i) && (return (1)); } else { ($silent) && (push @args, "-q"); ($lock =~ /^lock/i) && (push @args, "-l"); ($revision) && (push @args, "-r$revision"); $rv = system $RCS_CO, @args, $rcsfile; } syslog ('info', "RCS checkout: $rcsfile"); return ($rv); } sub RCSCheckIn { my ($rcsfile, $message) = @_; my (@args); my ($rv); if (!-w $rcsfile) { # error, not checked out for writing return (1); } ($message) && (push @args, "-m$message"); ($silent) && (push @args, "-q"); $rv = system $RCS_CI, @args, $rcsfile; syslog ('info', "RCS checkin: $rcsfile"); return ($rv); } # generic checkin/checkout sub CheckInOut { my ($file, $inout, $lock, $yes, $message, $revision) = @_; my ($rcsfile); my ($dbmd5, $instmd5, $resp); my ($rv) = 0; ($file, $rcsfile) = FileCvt ($file); $dbmd5 = DBGetFileAttr ($file, $installdb, "md5sum"); $instmd5 = GetInstalledMD5 ($file); if ($dbmd5 ne $instmd5) { syslog ('warning', "signature mismatch: $file"); if (!$yes && !$silent) { print "\n**********************************************************************\n"; print "WARNING: $file HAS CHANGED\n"; print "The installed version of this file has changed from the version stored\n"; print "in the repository. This probably means that someone (or some program)\n"; print "edited the file without using rcs.mgr or that you do not have\n"; print "permission to read the installed file.\n"; print "**********************************************************************\n"; print "Do you wish to continue this operation? [y/N]: "; chomp ($resp = ); } if (!($yes || (defined $resp && $resp =~ /^y/i))) { print "Bailing out due to signature mismatch.\n"; syslog ('warning', "exit on signature mismatch: $file"); closelog(); exit (1); } else { syslog ('warning', "override signature mismatch: $file"); } $resp = ""; } ($inout =~ /^in$/i) && ($rv = RCSCheckIn ($rcsfile, $message)); ($inout =~ /^out$/i) && ($rv = RCSCheckOut ($rcsfile, $revision, $lock)); return ($rv); } # initialize a repository, including the config file, etc. sub InitRCSRepository { my ($wd) = @_; if (-d $wd || -e $wd) { (!$silent) && (print "Can't initialize $wd: file or directory exists.\n"); syslog ('error', "repository already exists: $wd"); closelog(); exit (1); } else { if (!$silent) { my ($iuser, $igroup, $imode, $ieditor); print "Enter system defaults:\n"; print "User [".$DEFAULT_ATTR{"user"}."]: "; chomp ($iuser = ()); ($iuser eq "") && ($iuser = $DEFAULT_ATTR{"user"}); print "Group [".$DEFAULT_ATTR{"group"}."]: "; chomp ($igroup = ()); ($igroup eq "") && ($igroup = $DEFAULT_ATTR{"group"}); printf "Mode [%04o]: ", $DEFAULT_ATTR{"mode"}; chomp ($imode = ()); if ($imode eq "") { $imode = $DEFAULT_ATTR{"mode"}; } else { $imode = oct $imode; } print "Editor [$editor]: "; chomp ($ieditor = ()); if ($ieditor ne "" && !-x $ieditor) { print "$ieditor not found, using $editor instead\n"; $ieditor = ""; } ($ieditor eq "") && ($ieditor = $editor); # set for next section $DEFAULT_ATTR{"user"} = $iuser; $DEFAULT_ATTR{"group"} = $igroup; $DEFAULT_ATTR{"mode"} = $imode; $editor = $ieditor; } mkdir $wd, 0755; mkdir "$wd/RCS", 0755; open CONFIG, ">$wd/rcsmgr.conf"; print CONFIG "WORKING DIRECTORY = $wd\n"; print CONFIG "PATH = $rcs_path_sep\n"; print CONFIG "USER = ".$DEFAULT_ATTR{"user"}."\n"; print CONFIG "GROUP = ".$DEFAULT_ATTR{"group"}."\n"; printf CONFIG "MODE = %04o\n", $DEFAULT_ATTR{"mode"}; print CONFIG "EDITOR = $editor\n"; print CONFIG "INSTALLDB = $wd/installdb\n"; close CONFIG; } # create the installdb -- the only time we don't use the DB* routines open INST, ">$wd/installdb"; print INST "#" x 70; print INST "\n# rcs.mgr install database\n\n"; close INST; if (!$silent) { print "To use the repository system-wide, please move $wd/rcsmgr.conf\n"; print "to one of the following locations:\n"; foreach (@CONFIG) { ($_ =~ /\//) && (print " $_\n"); } print "If one of these files already exists, it will override the file just created.\n"; } syslog ('notice', "repository created: $wd"); closelog(); exit (0); } # Generate RCS log messages of a managed file sub LogFile { my ($file) = @_; my ($rcsfile); ($file, $rcsfile) = FileCvt ($file); if (!-r "RCS/$rcsfile,v") { die "Unable to read repository for $file: $!\n"; } system $RLOG, $rcsfile; } # Add file to the managed repository sub AddFile { my ($file, $defaults, $message) = @_; my ($rcsfile); my (%iattr, $imode); my (@args); if (!-w $installdb) { print "You do not have permission to update the installation database:\n$installdb: $!\n"; syslog ('notice', "DB access denied: $installdb"); closelog(); exit (1); } ($file, $rcsfile) = FileCvt ($file); (!$silent) && (print "This is the initial check in of $file.\n"); ($silent) && (push @args, "-q"); ($message) && (push @args, "-t-$message"); push @args, "-i"; if (-r $file) { system "/bin/cp", $file, $rcsfile; system $RCS_CI, @args, $rcsfile; $iattr{"md5sum"} = GetInstalledMD5 ($file); } else { (!$silent) && (print "$file does not yet exist - starting with an empty file.\n"); system $RCS, @args, $rcsfile; $iattr{"md5sum"} = $nullmd5; } $iattr{"file"} = $file; if ($installdb ne "") { if (!$defaults && !$silent) { print "Enter installation defaults:\n"; print "File editor [DEFAULT]: "; chomp ($iattr{"editor"} = ()); ($iattr{"editor"} eq "") && ($iattr{"editor"} = "DEFAULT"); print "User [".$DEFAULT_ATTR{"user"}."]: "; chomp ($iattr{"user"} = ()); ($iattr{"user"} eq "") && ($iattr{"user"} = $DEFAULT_ATTR{"user"}); print "Group [".$DEFAULT_ATTR{"group"}."]: "; chomp ($iattr{"group"} = ()); ($iattr{"group"} eq "") && ($iattr{"group"} = $DEFAULT_ATTR{"group"}); printf "Mode [%04o]: ", $DEFAULT_ATTR{"mode"}; chomp ($imode = ()); if ($imode eq "") { $iattr{"mode"} = $DEFAULT_ATTR{"mode"}; } else { $iattr{"mode"} = oct $imode; } print "Post-install commands [none]: "; chomp ($iattr{"commands"} = ()); } else { $iattr{"editor"} = "DEFAULT"; $iattr{"user"} = $DEFAULT_ATTR{"user"}; $iattr{"group"} = $DEFAULT_ATTR{"group"}; $iattr{"mode"} = $DEFAULT_ATTR{"mode"}; $iattr{"commands"} = $DEFAULT_ATTR{"commands"}; if (!$silent) { print "Adding $file to the repository with the following values:\n"; printf " Editor: %s\n", $iattr{"editor"}; printf " Ownership: %s:%s [%04o]\n", $iattr{"user"}, $iattr{"group"}, $iattr{"mode"}; print " Installation commands:\n"; foreach (split (';', $iattr{"commands"})) { $_ =~ s/^\s+//; print " -> $_\n"; } } } DBSetFileAttrList ($file, $installdb, %iattr); } syslog ('notice', "add file: $file"); } # check out and edit a file sub EditFile { my ($file, $revision, $message, $yes) = @_; my ($rcsfile) = ""; my ($pid); my ($badco) = 1; my ($resp) = ""; my (%attr); ($file, $rcsfile) = FileCvt ($file); if (!-e "RCS/$rcsfile,v") { (!$silent) && (print "$file not found in repository.\n"); closelog(); exit (1); } %attr = DBGetFileAttrList ($file, $installdb); $badco = CheckInOut ($file, "out", "lock", $yes, "", $revision); if (!$badco) { $pid = fork; if ($pid) { waitpid $pid, 0; if (!$yes && !$silent) { print "Do you want to leave $rcsfile\n"; print "in the repository working directory for future editing?\n"; print "(Selecting 'no' will check in any changes you have made.) [y/N]: "; chomp ($resp = ); } if ($yes || $resp eq "" || $resp =~ /^n/i) { RCSCheckIn ($rcsfile, $message); (!$silent) && (print "You may now install $file.\n"); } else { if (!$silent) { print "Leaving $rcsfile in current directory for future editing.\n"; print "Please use '$RCS_CI $rcsfile' before installing.\n"; } } } else { my ($logeditor, @editorcmd); if ($attr{"editor"} eq "DEFAULT") { @editorcmd = ($editor, $rcsfile); $logeditor = $editor; } elsif (defined $attr{"editor"} && -x $attr{"editor"}) { @editorcmd = ($attr{"editor"}, $rcsfile); $logeditor = $attr{"editor"}; } else { @editorcmd = ($editor, $rcsfile); $logeditor = $editor; } syslog ('notice', "edit file: $file [$logeditor]"); exec @editorcmd; } } else { (!$silent) && (print "RCS checkout failed for $file.\n"); syslog ('warning', "checkout failed: $file"); closelog(); exit (1); } } # show changes between the installed file and the one in the repository sub DiffFile { my ($file, $revision) = @_; my ($rcsfile, $badco); ($file, $rcsfile) = FileCvt ($file); syslog ('notice', "diff requested: $file"); $badco = CheckInOut ($file, "out", "nolock", 1, "", $revision); if ($badco) { (!$silent) && (print "RCS checkout failed on $rcsfile.\n"); syslog ('warning', "checkout failed: $file"); closelog(); exit (1); } system ($DIFF, $rcsfile, $file); unlink ($rcsfile); exit (0); } # Install a managed file in its live location sub InstallFile { my ($file, $revision, $yes) = @_; my ($rcsfile, $uid, $gid); my ($badco) = 1; my (%attr); my ($inst) = ""; my (@insts, $i, $resp); my ($modeswitch); ($file, $rcsfile) = FileCvt ($file); %attr = DBGetFileAttrList ($file, $installdb); # if we've not got a numeric UID/GID, look it up # except on Solaris, where install wants a symbolic name... if ($attr{"user"} =~ /\D/ && $uname !~ /SunOS/) { $uid = (getpwnam $attr{"user"})[2]; } else { $uid = $attr{"user"}; } if ($attr{"group"} =~ /\D/ && $uname !~ /SunOS/) { $gid = (getgrnam $attr{"group"})[2]; } else { $gid = $attr{"group"}; } # check out the requested version of the file $badco = CheckInOut ($file, "out", "nolock", $yes, "", $revision); if ($badco) { (!$silent) && (print "RCS checkout failed on $rcsfile.\n"); syslog ('warning', "checkout failed: $file"); closelog(); exit (1); } # install the file using system install program (!$silent) && (print "Installing $rcsfile as $file.\n"); if ($uname =~ /SunOS/) { # SunOS 5.8 install is a fairly braindead shell script # it requires the filename to be the same in the source # and target locations, so we must rename it to the basic # before we install it my ($dir) = $file; my ($tmpfile) = $file; $dir =~ s/\/[^\/]+$//; $tmpfile =~ s/$dir\///; rename $rcsfile, $tmpfile; $modeswitch = sprintf "%04o", $attr{"mode"}; system $INSTALL, "-f", $dir, "-m", $modeswitch, "-u", $uid, "-g", $gid, $tmpfile; rename $tmpfile, $rcsfile; } else { # this works on: # FreeBSD, NetBSD, OpenBSD, Linux, Darwin (more?) $modeswitch = sprintf "%04o", $attr{"mode"}; system $INSTALL, "-o$uid", "-g$gid", "-m$modeswitch", $rcsfile, $file; } syslog ('notice', "install: $file [$uid/$gid/$modeswitch]"); # run any post-install commands if the user says yes if (defined $attr{"commands"} && $attr{"commands"} ne "") { $resp = 'n'; @insts = split /;/, $attr{"commands"}; (!$silent) && (print "The following post-installation commands should be run:\n"); if (!$yes && !$silent) { foreach $i (@insts) { $i =~ s/^ //; print " -> $i\n"; } print "Run them now [Y/n]? "; chomp ($resp = ); } if ($yes || $resp !~ /^n/i) { (!$silent) && (print "Running post-installation commands:\n"); syslog ('notice', "running post-installation commands: $file"); foreach $i (@insts) { $i =~ s/^ //; (!$silent) && (print " $i\n"); system $i; } } else { if (!$silent) { print "Deferring post-installation commands.\n"; } syslog ('notice', "deferring post-installation commands: $file"); } } # update md5 sum if available UpdateMD5 ($file); # checkin properly, then set default branch if revision was selected unlink $rcsfile; if ($revision) { (!$silent) && (system $RCS, "-b$revision", $rcsfile); ($silent) && (system $RCS, "-q", "-b$revision", $rcsfile); syslog ('notice', "rcs head branch is $revision: $file"); } } # remove file from the repository sub RemoveFile { my ($file) = @_; my ($rcsfile, $resp); my (@instdb); my ($mode); if ($silent) { print "Sorry, this command requires user interaction.\n"; closelog(); exit (1); } ($mode) = (stat $installdb)[2]; if (!-w _) { print "You do not have permission to update the installation database.\n"; syslog ('notice', "DB access denied: $installdb"); closelog(); exit (1); } ($file, $rcsfile) = FileCvt ($file); if (-e "RCS/$rcsfile,v") { print "You are about to PERMANENTLY remove $file from the repository.\n"; print "Are you sure you want to do this? [y/N]: "; chomp ($resp = ); if ($resp =~ /^y/i) { unlink "RCS/$rcsfile,v"; print "$file removed from repository.\n"; # remove from install db DBRemoveFile ($file, $installdb); syslog ('warning', "remove file: $file"); } else { print "Leaving $file in the repository.\n"; } } else { print "$file not found in repository.\n"; } } # change the attributes of a file in the repository sub ChangeAttr { my ($file, $attrlist) = @_; my (%oattr, %iattr, $imode); %oattr = DBGetFileAttrList ($file, $installdb); if (!$silent) { print "Enter new installation defaults:\n"; print "File editor [".$oattr{"editor"}."]: "; chomp ($iattr{"editor"} = ()); ($iattr{"editor"} eq "") && ($iattr{"editor"} = $oattr{"editor"}); print "User [".$oattr{"user"}."]: "; chomp ($iattr{"user"} = ()); ($iattr{"user"} eq "") && ($iattr{"user"} = $oattr{"user"}); print "Group [".$oattr{"group"}."]: "; chomp ($iattr{"group"} = ()); ($iattr{"group"} eq "") && ($iattr{"group"} = $oattr{"group"}); printf "Mode [%04o]: ", $oattr{"mode"}; chomp ($imode = ()); if ($imode eq "") { $iattr{"mode"} = $oattr{"mode"}; } else { $iattr{"mode"} = oct $imode; } if ($oattr{"commands"} ne "") { print "Post-install commands [".$oattr{"commands"}."]: "; } else { print "Post-install commands [none]: "; } chomp ($iattr{"commands"} = ()); ($iattr{"commands"} eq "") && ($iattr{"commands"} = $oattr{"commands"}); } else { %iattr = ParseAttrOption ($attrlist, %oattr); } # take care of non-settables: $iattr{"file"} = $oattr{"file"}; $iattr{"md5sum"} = $oattr{"md5sum"}; DBSetFileAttrList ($file, $installdb, %iattr); syslog ('notice', "attribute change: $file"); } # generate an index of the RCS repository # this excludes files that have no path seps (other than the ",v") on the # assumption that they are info files or other such beasts and should # -not- be installed in the root directory. sub RCSIndex { my ($dir) = @_; my (@rcsdir, $rcsfile); opendir RCSDIR, $dir or die "Unable to read repository '$dir': $!\n"; @rcsdir = readdir RCSDIR; closedir RCSDIR; @rcsdir = sort @rcsdir; print "Index of managed files:\n"; foreach (@rcsdir) { if ($_ ne "." && $_ ne "..") { # exclude the self & parent dirs my ($file) = substr $_, 0, (length ($_) - 2); if ($file =~ /$rcs_path_sep/) { # no root-dir entries ($file, $rcsfile) = FileCvt ($file); print "$file\n"; } } } } ######################### # MD5 management routines sub GetInstalledMD5 { my ($file) = @_; my ($md5); if (!-e $file) { return $nullmd5; } if (!-r $file) { return "x"; } if ($MD5) { open (MD5, "-|") || exec $MD5, $file; while () { if ($_ =~ /$file/) { chomp; ($md5) = (split (/\s+/, $_))[3]; } } close (MD5); } elsif ($MD5SUM) { open (MD5, "-|") || exec $MD5SUM, $file; while () { if ($_ =~ /$file/) { ($md5) = (split (/\s+/, $_))[0]; } } close (MD5); } else { $md5 = $nullmd5 } return ($md5); } sub UpdateMD5 { my ($file) = @_; my ($newmd5sum); if ($MD5 eq "" and $MD5SUM eq "") { print "Your system does not appear to have an MD5 program available.\n"; return; } if ($file eq "*") { my (@files); @files = DBGetFileList ($installdb); foreach (@files) { UpdateMD5 ($_); } } else { $newmd5sum = GetInstalledMD5 ($file); DBSetFileAttr ($file, $installdb, "md5sum", $newmd5sum); syslog ('info', "update signature: $file"); } } sub CheckMD5 { my ($file) = @_; my (%attr, $md5sum); if ($MD5 eq "" and $MD5SUM eq "") { print "Your system does not appear to have an MD5 program available.\n"; return; } if ($file eq "*") { my (@files); @files = DBGetFileList ($installdb); foreach (@files) { CheckMD5 ($_); } } else { %attr = DBGetFileAttrList ($file, $installdb); $md5sum = GetInstalledMD5 ($file); if ($md5sum eq "x") { print "$file: permission denied\n"; } elsif ($md5sum ne $attr{"md5sum"}) { print "$file has changed from repository\n"; } } } ############################################################ # INSTALL DATABASE FILE ROUTINES sub DBGetAllFileAttrs { my ($installdb) = @_; my (@a, $attrs); sysopen (DB, $installdb, O_RDONLY|O_CREAT|LOCK_EX, 0660); while () { chomp; if ($_ ne "" && $_ !~ /^#/) { my (%b); @a = split (':', $_); $b{"file"} = $a[0]; $b{"user"} = $a[1]; $b{"group"} = $a[2]; $b{"mode"} = $a[3]; $b{"md5sum"} = $a[4]; $b{"commands"} = $a[5]; if (defined $a[6]) { $b{"editor"} = $a[6]; } else { $b{"editor"} = "DEFAULT"; } %{$attrs}->{$a[0]} = %b; } } close (DB); return $attrs; } sub DBRemoveFile { my ($file, $installdb) = @_; my (@h); sysopen (DB, $installdb, O_RDWR|O_CREAT|LOCK_EX, 0660); while () { if ($_ !~ /^$file:/) { push @h, $_; } } seek (DB, 0, 0); truncate (DB, 0); print DB @h; close (DB); } sub DBGetFileList { my ($installdb) = @_; my (@files); sysopen (DB, $installdb, O_RDONLY|O_CREAT|LOCK_EX, 0660); while () { chomp; if ($_ ne "" && $_ !~ /^#/) { my ($f) = (split (':', $_))[0]; push @files, $f; } } close (DB); return @files; } sub DBGetFileAttrList { my ($file, $installdb) = @_; my (@a, %attr); sysopen (DB, $installdb, O_RDONLY|O_CREAT|LOCK_EX, 0660); while () { chomp; if ($_ =~ /^$file:/) { @a = split (':', $_); $attr{"file"} = $file; $attr{"user"} = $a[1]; $attr{"group"} = $a[2]; $attr{"mode"} = oct $a[3]; $attr{"md5sum"} = $a[4]; $attr{"commands"} = $a[5]; if (defined $a[6]) { $attr{"editor"} = $a[6]; } else { $attr{"editor"} = "DEFAULT" } } } close (DB); return %attr; } sub DBSetFileAttrList { my ($file, $installdb, %attr) = @_; my (@h, $entry, $found); $entry = sprintf "%s:%s:%s:%04o:%s:%s:%s", $attr{"file"}, $attr{"user"}, $attr{"group"}, $attr{"mode"}, $attr{"md5sum"}, $attr{"commands"}, $attr{"editor"}; sysopen (DB, $installdb, O_RDWR|O_CREAT|LOCK_EX, 0660); $found = 0; while () { if ($_ =~ /^$file:/) { push @h, "$entry\n"; $found = 1; } else { push @h, $_; } } push @h, "$entry\n" unless $found; seek (DB, 0, 0); truncate (DB, 0); print DB @h; close (DB); } sub DBGetFileAttr { my ($file, $installdb, $attr) = @_; my (%attrlist); %attrlist = DBGetFileAttrList ($file, $installdb); return ($attrlist{"$attr"}); } sub DBSetFileAttr { my ($file, $installdb, $attr, $value) = @_; my (%attrlist); %attrlist = DBGetFileAttrList ($file, $installdb); $attrlist{"$attr"} = $value if (defined $attrlist{"$attr"}); DBSetFileAttrList ($file, $installdb, %attrlist); } ################################################################### =pod =head1 NAME rcs.mgr - manage an RCS repository of configuration files =head1 SYNOPSIS B B Typically: B B<-e>, B<--edit> F B B<-i>, B<--install> F =head1 DESCRIPTION B is designed to manage an RCS repository of configuration files. Typically a working directory will be created for a workstation under which the RCS directory is created. For example F will be the working directory and F will contain the RCS files. Managed files in the repository will be named with their full path names, with slashes changed to another character, by default a colon (:) (F becomes F<:etc:printcap>). This allows a repository to contain files for a single workstation from multiple directories without having to include any directory in its entirety. A configuration file (located in F, F, or F in the local directory) is used to provide defaults for the program. Values are specified as "variable = value". The most important values are PATH (designating the converted path separation character), WORKING DIRECTORY (designating the parent directory of the RCS repository), and INSTALLDB (designating the database of installation defaults for each file). When a repository is created with the C<--initialize> (C<-I>) command, the F and F files are created in the designated directory. F should be moved to F or in order to be able to run the program from anywhere on the system. The following options are recognized by B. In all cases where a file name is required, it may be specified either in the live form (full path name) or repository form (path slashes replaced with the repository's path separator). =over 8 =item B<-a, --add> file Add the named file to the repository of managed files. If the file exists and is readable by the current user, it is copied to the working directory. If it does not exist, an empty file is created. The file in the working directory is then checked in and the user is asked to provide the initial check in description of the file. The user is also requested to provide the user, group, mode, post-installation commands, and alternate editor associated with the file for entry into the install database. =item B<-C, --change> file Change the installation database attributes of a file. With no other options, it sets the installation attributes to the defaults as stored in F or the internal program defaults. With the C<--attributes> option, it sets the attributes to those specified. =item B<-c, --config> file Use an alternate configuration file. If found, this file takes precedence over any other configuration file that may be present on the system. =item B<-d, --diff> file Show the differences (as per diff(1)) between the latest (or requested) version of the file in the repository and the currently installed version of the file. This can be used as a poor man's rcsdiff(1) between an earlier version of the file and the latest or as a way of seeing what changes have been made without B if C<--check> reports a signature difference. =item B<-e, --edit> file Check out the named file and edit it. The program used to edit the file is determined in this order: 1. If the file has an 'editor' attribute set, this program is used. 2. If the EDITOR environment variable is set, this is used. 3. The default editor (usually vi(1)) is used. When the editor process exits, the user is asked whether to save this file for further editing. If so, B will exit after giving a reminder to check in changes with ci(1). If the user is done editing, B will call ci(1) directly. =item B<-g, --group> group Specify a group name or numeric ID for ownership of the installed file. This option is a shortcut for specifying the group in the C<--attributes> command and is deprecated. =item B<-h, --help> Print out a brief help synopsis and then exits. =item B<-I, --initialize> directory Initialize the working directory named. This creates the directory and the RCS repository directory and creates the F file that overrides program defaults and the installation database. This step is strongly advised but not necessary in creating the repository of managed files. The working directory and RCS repository must be created by hand if this step is not performed. =item B<-i, --install> file Install the named file. It will be installed in the directory determined by its repository name. The user will be asked whether any post-installation commands found in the installation database should be run at this time. =item B<-L, --showlog> file Run rlog(1) on the named file, giving a history of all RCS log messages. =item B<-l, --list> List the managed files in the RCS repository in the current working directory. The output will give the names of files in their final locations, not the repository name. =item B<-M, --message> message Set the checkin log message to the string given. If the message text is present, the user will not be prompted for a message. =item B<-m, --mode> mode Specify the mode (in octal) of the installed file. See chmod(1). The default is 0444. This option is a shortcut for specifying the mode in the C<--attributes> command and is deprecated. =item B<-r, --remove> file Remove the named file from the RCS repository and the install database. Do not use this option lightly, as it removes the entire history of the file. =item B<-s, --silent> Run silently and non-interactively for those commands where it is appropriate (not available for log, list, or remove commands). If unexpected conditions are found while running (e.g., MD5 signature mismatch), the program exits. =item B<-u, --user> user Specify the user name or numeric ID for ownership of the installed file. This option is a shortcut for specifying the user in the C<--attributes> command and is deprecated. =item B<-v, --version, --revision> version Specify an RCS revision number to edit or install. Specifying a revision earlier than the latest while editing will create a branch in the repository. If an earlier branch is installed, it will become the default branch for all future operations (this may be changed by running rcs(1) directly). =item B<-y, --yes, --defaults, --runcommands> Bypass all prompts and provide the default answer. When adding a file to the repository, default values are used for ownership, permissions, and post-installation commands. When editing a file, assume that the file should be checked in after editing. When installing a file, run the post-installation commands listed for the file. =back The following options are available in long format only. There is getting to be too many options for short format to make any sense, and these options are less often used. =over 8 =item B<--attributes> attr1=value,attr2=value... Set the installation database attributes for the file. The attributes that can be changed are B, B, B, B, and B. See the section on the installation database for further details. =item B<--checkout> file Check out a file for editing and exit, leaving the file in the working directory. This is identical to entering the working directory and running co(1) for the file, except that the B niceties are present. =item B<--checkin> file Check in a file that has been checked out using either the B<--checkout> option or left in the working directory after an editing session. =item B<--checkall> Check the MD5 signatures of the installed files and compare them against the signatures stored in the install database for all managed files. For those that are different, a warning is issued. This is a quick and dirty way to determine if a file has changed without going through the B management. It is B a replacement for an intrusion detection system like Tripwire. Since B requires a writeable install database and repository, there is nothing to keep intruders from modifying the signature stored in the database. =item B<--check> file Check the MD5 signature for a single file. =item B<--updateall> Update all the MD5 signatures in the installation database on the assumption that the currently installed version is correct. =item B<--update> file Update the MD5 signature for a single file. =back =head2 Operation First, a working directory for the repository is created and initialized with the C<--initialize> (C<-I>) option. This directory may be located wherever it is most convenient (e.g., under F, on an NFS-mounted volume with repositories for other workstations, etc.). Typically a managed file is added to the repository with the C<--add> (C<-a>) option. This puts the original version of the file as the first version in the repository, making rollback to the initial state possible (see the C<--version> (C<-v>) option). Files may be edited either with the C<--edit> (C<-e>) option or by specifying the C<--checkout> option and making the desired modifications. In the latter case (or if the file has been retained in the working directory after an edit session), the file may be checked in with the C<--checkin> option. When an updated file is ready for installation, B is called with the C<--install> (C<-i>) option. The file is checked out for reading and the installation database is consulted to find the file ownership and post-installation commands associated with the file. install(1) is called to do the actual installation, and the user is asked whether any post-installation commands should be run. When the file has been installed, the checked out copy is deleted. A file may be removed from the repository with the C<--remove> (C<-r>) option. Do not use this feature lightly. In practice, the lifecycle of a particular file within a repository will consist of an add command: rcs.mgr --add /path/to/file Followed by an indefinite repition of the edit and install commands: rcs.mgr --edit /path/to/file rcs.mgr --install /path/to/file =head2 Configuration File The configuration file (F) for a repository is a simple file that contains "name = value" pairs. It is created when a repository is initialized and may be edited. For simple usage, this file is not required, however to access a repository from anywhere on the system, this file must be present and must live in F or F and must specify the working directory. The following values are set: WORKING DIRECTORY = /path/to/directory PATH = : USER = root GROUP = root MODE = 0444 EDITOR = /usr/bin/vi INSTALLDB = /path/to/directory/installdb The user, group, mode, and editor values are used as defaults when adding files to the repository (and the installation database) and may be changed either at addition or with the C<--attributes> option. If the editor associated with a file is 'DEFAULT', the value in this file or the EDITOR environment variable will be used. The working directory must be specified and F must be present in F or F in order to be able to run B from any directory on the system. As a last resort, it looks for the configuration file in the current directory or uses defaults within the program. =head2 Installation Database The installation database resides in the working directory and is named F by default. As files are added to the repository, the default user, group, and mode of the file are recorded, along with any commands that should be run after the file is installed (e.g. newaliases(1) for F). The MD5 signature of the file (assuming F or F is available on the system) is stored every time the file is installed as a simple way to determine if changes have been made without the use of B. If an MD5 program is not available, the MD5 signature of a zero length file is recorded (d41d8cd98f00b204e9800998ecf8427e). Starting with B 3.0, a final field is also stored to indicate a non-default editor that should be used when editing the file. The format of the database is filename:user:group:mode:md5_signature:post-install commands:editor Blank lines and lines beginning with '#' are ignored. Multiple post-installation commands may be specified with a semicolon (';') between each. A file need not have an entry in the database (although if it was added with the C<-a> command, there will be an entry). In this case, the default values from F are used. It is highly advised that all files be present in the installation database; in future releases the database may become a requisite part of the system. The fields for any file may be changed by using the C<--attributes> and the C<--change> options in conjunction. The fields that may be specified in the C<--attributes> option are: =over 4 =item B The user ownership of the file. =item B The group ownership of the file. =item B The octal mode bits of the file. =item B The post-installation commands to be run for the file. =item B The editor to use for the file. =back =head1 BUGS & GOTCHAS Replacing path name slashes with another character will lead to problems with files that include that character in their live name. The only option in this case is to use a different path separator in the repository. Multi-character path separators may be chosen to minimize the chances of name-space collision at the risk of increasing obfuscation. Separators with forward slashes in them in any form just ain't never gonna work. If a previous version of a file is checked out and changed, it will not become the default branch B. In particular, this means that the maintainer must pay attention to what version number is reported at check in and use that version with the install command. This also has the effect that unless specified otherwise, edits on a non-default branch must all be told what version to check out until the file is installed. Pay attention. =head1 COPYRIGHT Copyright (c) 2000-2002, John "Rowan" Littell. All rights reserved. Redistribution of this script, either in source or any compiled binary format, with or without modificiation, is permitted provided the following conditions are met: =over 4 =item 1. Redistributions must retain the above copyright notice, this list of conditions, and the following disclaimer either within the script itself or within the documentation or other materials accompanying the script. =item 2. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. =back THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. =head1 SEE ALSO rcs(1), ci(1), co(1), rlog(1), install(1) =head1 AUTHOR John "Rowan" Littell =cut