#!/usr/bin/ksh # Solaris /bin/sh explodes on the perfectly legal remote() function :-( # # Freshmeat release number: 1.1 # # =head1 NAME # # syncopt - keep per-machine /opt in sync with master copy # # =head1 SYNOPSIS # # syncopt [options] [packages...] # # =head1 DESCRIPTION # # I is a command for keeping host local package installations # synchronised with a master host. # More detailed discussion of its use and the practices associated with # it is at this web page: # # http://www.cskk.ezoshosting.com/cs/css/syncopt.html # # The intent is that packages built from source # (and to a limited extent other packages with relocatable installs) # need only be installed once on a central host, # and that ``client'' machines get a local copy or a symbolic link # as the administrator sees fit. # # The basic scheme is that packages get built # and installed as B-I> # and then copied to a central spot # such as B/I-I>, # leaving a symlink from B to the central package. # In this way things think they # live in B # and become local or remote by either symlinking from B to the central copy # or by installing an exact copy # of B/I-I> in . # # The purpose of B is to maintain that arrangement # in a convenient and automatic fashion. # # It would normally be run automatically on client machines via cron, # or batch dispatched on all clients by hand via ssh after a new package install. # : ${TMPDIR:=/tmp} : ${RSYNC_RSH:=sshto}; export RSYNC_RSH : ${VENDOR_RELEASE:=''} cmd=`basename "$0"` || cmd=$0 umask 2 # installation directory : ${SYNCOPT_OPT:=/opt} cf=$SYNCOPT_OPT/.syncopt if [ -z "$ARCH" ] then if [ -s "$SYNCOPT_OPT/ARCH" ] then ARCH=`cat "$SYNCOPT_OPT/ARCH"` || { echo "$cmd: can't infer \$ARCH from $SYNCOPT_OPT/ARCH file" >&2 exit 1 } else ARCH=`uname -s` fi export ARCH fi : ${SYNCOPT_PATH:=/u/syncopt} # NB: space separated : ${SYNCOPT_SUBDIRS:="$ARCH common"} # NB: space separated : ${SYNCOPT_RSYNC:=rsync} : ${SYNCOPT_RSYNCOPTS:=""} : ${SYNCOPT_SSH:=$RSYNC_RSH} cpu=`uname -p` || exit 1 # basic "trim comments" code sedpreamble='y/ / / s/^ *// s/^#.*// /^$/d s/ *$// s/ */ /g' # extract envvars sedscript="$sedpreamble /^PATH=/b setvar /^SUBDIRS=/b setvar /^RSYNC=/b setvar /^RSYNCOPTS=/b setvar d :setvar s/'/'\\\\''/g s/^\\([^=]*\\)=\\(.*\\)/SYNCOPT_\\1='\\2'; export SYNCOPT_\\1/" sedvars=`[ ! -s "$cf" ] || sed -e "$sedscript" "$cf"` \ || { echo "$cmd: can't parse $cf" >&2; exit 1; } eval "$sedvars" usage="Usage: $cmd [-n] [-f] [l] [-R rsync-path] [-x] [-v] [items...] -f Force action. -l Localise the specified items. -L Rsync's --copy-links option. -R rsync-path Rsync command path. Default, from \$SYNCOPT_RSYNC: $SYNCOPT_RSYNC -r [user@]host Remote host with the master files. -n No action - echo necessary actions. -x Force action with tracing. -v Verbose." trace=eecho # set-x # set to nothing when debugged localise= vflag= [ -t 1 ] && vflag=-v # =head1 OPTIONS # # =over 4 # hadmode= badopts= while : do case $1 in # =item B<-f> # # Force installation. # The default action is to change nothing, # merely reciting the actions which would be performed. # -f) hadmode=1 trace= ;; # =item B<-x> # # Trace execution. Implies B<-f>. # Report actions as they are taken. # -x) hadmode=1 trace=set-x ;; # =item B<-l> # # Default to installing all packages locally. # This is almost never what you want at the command line # and I may remove this option. # At need a suitable # # * local # # line can be placed in the B configuration file. # -L) SYNCOPT_RSYNCOPTS="$SYNCOPT_RSYNCOPTS $1" ;; # =item B<-L> # # Pass the B<-L> (aka B<--copy-links>) option to rsync(1). # This treats symlinks as regular files # and should render local installs that include symlinks to other, nonlocal, syncopt packages # robust against loss of the central server. # For example, B frequently has a bunch of symlinks like: # # xpdf -> /opt/xpdf/bin/xpdf # # to make pakage executables easily available. # Without B<-L>, a local B would not mean a local B executable. # There is some space bloat with use of this option. # -l) localise=1 ;; # =item B<-n> # # No action. This is the default. # Merely recite the actions which would be performed. # -n) hadmode=1 trace=eecho ;; # =item B<-R> I # # Path to the rsync(1) command. # useful to run a particular rsync version # or for bootstrapping. # -R) SYNCOPT_RSYNC=$2; shift ;; # =item B<-r> I # # Use the host I as the master repository host. # Normally B will expect direct access to the master copies # or the packages via the normal filesystem namespace, # usually in B # which is typically NFS mounted. # Sometimes this isn't feasible. # The B<-r> option causes B to use ssh(1) # to access a remote host to obtain packages. # -r) remote=$2; shift ;; # =item B<-v> # # Verbose. # Produce more output during the run. # -v) verbose=1 vflag=-v ;; --) shift; break ;; -?*) echo "$cmd: unrecognised option: $1" >&2; badopts=1 ;; *) break ;; esac shift done # =back # if [ $# = 0 ] && [ $localise ] then echo "$cmd: -l requires specific enumeration of targets targets" >&2 badopts=1 fi [ $hadmode ] || { echo "$cmd: no mode supplied; one of -f, -n or -x required" >&2 badopts=1 } [ $badopts ] && { echo "$usage" >&2; exit 2; } # ensure there is a real local /opt [ -d "$SYNCOPT_OPT/." ] \ || { $trace mkdir "$SYNCOPT_OPT" && $trace chmod 755 "$SYNCOPT_OPT"; } \ || { echo "$cmd: missing target dir $SYNCOPT_OPT" >&2 exit 1 } # inrepository [host:]repository cmd [args...] # inrepository [host:]repository -c shcmd inrepository() { _wr_rep=$1; shift [ "x$1" = x-c ] && { shift; set -- /bin/sh -c "$@"; } case "$_wr_rep" in *:*) _wr_remote=`expr "x$_wr_rep" : 'x\([^:]*\):.*'` || exit 1 _wr_rep=` expr "x$_wr_rep" : 'x[^:]*:\(.*\)'` || exit 1 ;; *)_wr_remote= ;; esac if [ -z "$_wr_remote" ] then ( cd "$_wr_rep" || exit 1 exec "$@" ) _wr_xit=$? else _wr_qrep=`shqstr "$_wr_rep"` || exit 1 _wr_rcmd=`shqstr "$@"` || exit 1 eval "$SYNCOPT_SSH "'"$_wr_remote" "cd $_wr_qrep || exit 1; $_wr_rcmd"; _wr_xit=$?' fi return $_wr_xit } # readsymlink indir name rreadsymlink() { inrepository "$1" \ perl -e '$_=readlink($ARGV[0]) || die "readlink($ARGV[0]): $!\n"; print "$_\n"; exit 0; ' "$2" } readsymlink() { rreadsymlink . "$1" } # issymlink rissymlink() { rreadsymlink "$1" "$2" >/dev/null 2>&1 } issymlink() { rissymlink . "$1" } tmppfx=$TMPDIR/$cmd$$ trap 'rm -f "$tmppfx"*' 0 trap 'rm -f "$tmppfx"*; exit 1' 1 2 13 15 # obtain list of names if none supplied if [ $# = 0 ] then set -- ` for rep in $SYNCOPT_PATH do inrepository "$rep" -c "for d in $SYNCOPT_SUBDIRS do ( [ -d \\"\\$d/.\\" ] && cd \\"\\$d\\" && ls ) done " done \ | sort -u ` fi # construct mapping of names to locations namelist=${tmppfx}nl for rep in $SYNCOPT_PATH do inrepository "$rep" \ -c "for subdir in $SYNCOPT_SUBDIRS do ( [ -d \"\$subdir/.\" ] && cd \"\$subdir\" && ls ) | sed \"s|.*|& \$subdir|\" done" | sed "s|.* |&$rep/|" done >"$namelist" # findmaster thing -> repository-location # locates _first_ (preferred) mention of thing in location map findmaster() { _fm_name=$1; shift awk "\$1 == \"$_fm_name\" { print \$2; exit; }" <"$namelist" } xit=0 # work in the specified /opt dir cd "$SYNCOPT_OPT" || exit 1 # for every target for thing do thingrep=`findmaster "$thing"` || exit 1 if [ -z "$thingrep" ] then echo "$cmd: can't find master for \"$thing\", skipping" >&2 xit=1 continue fi ##echo "$thing -> $thingrep" >&2 # local instance of thing ldir=$SYNCOPT_OPT # get unversioned name for $thing case "$thing" in *-[0-9]*) # using sed instead of expr to do nongreedy prefix match nvthing=`echo "$thing" | sed 's/-[0-9].*//'` || exit 1 thingv=`echo "$thing" | sed "s/^$nvthing-//"` || exit 1 if [ -z "$nvthing" -o -z "$thingv" ] then echo "$cmd: empty nvthing [$nvthing] or thingv [$thinv] !!" >&2 echo " SKIPPING $thing !!" >&2 continue fi ;; *) nvthing=$thing thingv= ;; esac ############################## # determine default behaviour: # whether to localise by default makelocal=$localise case "$thingrep" in *:*) makelocal=1 ;; esac [ $makelocal ] || issymlink "$thing" || [ ! -d "$thing" ] || makelocal=1 # what version to make the default for this host version= # leave this item alone? nosync= # =head1 CONFIGURATION FILE # # The defaults for B may be tuned by editing the file # B. # This is a simple text file with one line directives in it. # Directives are read in order, # with the I matching directive having precedence. # # Blank lines and lines commencing with an octothorpe ('#') are comments, and skipped. # # Lines of the form: # # var=value # # are read once at the start of the script to set environment parameters. # I may take the names B, B, B or B # to set or override the environment variables # B<$SYNCOPT_PATH>, # B<$SYNCOPT_RSYNC>, B<$SYNCOPT_RSYNCOPTS> or B<$SYNCOPT_SUBDIRS> respectively. # # Other lines are of the form: # # package mode # # or # # package version mode # # where I is the name of the package # (eg "B" or "B"), # I is the optional package revision, # and I is one of B for a local copy of the package, # B for a symbolic link, # and B # to have syncopt leave this package alone. # # If the I is supplied, # B uses that to specify the default package version # for the unversioned symlink: # # /opt/package -> package-version # awkf="BEGIN { thing=\"$thing\" nvthing=\"$nvthing\" thingv=\"$thingv\" } "' { useme=0 } $1 == "*" { useme=1 } NF == 1 && $1 == nvthing { print "nosync=" } $1 == thing || ( $1 == nvthing && ( $2 == thingv || $2 ~ /^[^0-9]/ ) ) { useme=1 print "nosync=" } useme > 0 && $NF == "nosync" { print "nosync=1" } useme > 0 && $NF == "local" { print "makelocal=1" } useme > 0 && $NF == "remote" { print "makelocal=" } $1 == thing && $2 ~ /^[0-9]/ { print "version=" $2 } thingv == "" && NF == 3 { print "makelocal=" } ' ##echo "awkf=[$awkf]" >&2 opts=`[ ! -s "$cf" ] || sed -e "$sedpreamble" "$cf" | awk "$awkf"` \ || { echo "$cmd: awk of config file [$cf] FAILED, skipping $thing !!" >&2 xit=1 continue } if [ -n "$opts" ] then ##echo "opts=[$opts]" >&2 eval "$opts" \ || { echo "$cmd: eval of options FAILED, skipping $thing" >&2 xit=1 continue } fi ##echo "thing=$thing makelocal=$makelocal thingv=$thingv vnthing=$nvthing version=$version" # skip this? [ $nosync ] && { echo "NOSYNC $thing"; continue; } # versioned item? make link and continue if [ -n "$version" ] then vlink=$thing-$version if llink=`readsymlink "$thing" 2>/dev/null` \ && [ "x$vlink" = "x$llink" ] then : leave it alone else $trace rm -rf "$thing" $trace ln -s "$vlink" "$thing" || xit=1 fi continue fi # files - usually just /opt/ARCH if inrepository "$thingrep" test -f "$thing" then $trace $SYNCOPT_RSYNC -a $vflag $SYNCOPT_RSYNCOPTS "$thingrep/$thing" "$thing" continue fi ############################################ # now talking about dirs or symlinks to dirs # if local end is not symlink, make local copy issymlink "$thing" || [ ! -d "$thing" ] || makelocal=1 if [ -z "$makelocal" ] then rlink=`rreadsymlink "$thingrep" "$thing" 2>/dev/null` \ || rlink=$thingrep/$thing if llink=`readsymlink "$thing" 2>/dev/null` \ && [ "x$rlink" = "x$llink" ] then : leave it alone else $trace rm -rf "$thing" $trace ln -s "$rlink" "$thing" || xit=1 fi continue fi # either $makelocal or the remote end is not a symlink # ==> we need a local copy $trace rm -f "$thing" [ -d "$thing/." ] || $trace mkdir "$thing" $trace $SYNCOPT_RSYNC -a $vflag --delete $SYNCOPT_RSYNCOPTS "$thingrep/$thing" . done exit $xit # =head1 ENVIRONMENT # # =over 4 # # =item B # # The architecture name for this machine. # The default is the output of ``B'', # although on systems I administer I arrange this to be a string of the form # B.I.I>, for example B. # # =item B # # This is the installation directory. # The default is B. # # =item B # # A space separated list of repositories to search for packages. # A repository is either a directory name # or a remote name like "B". # A machine not on a fixed LAN might use the latter form. # Packages found in earlier repositories in this path # are chosen over packages found later in the path. # The default is "B". # # =item B # # This may be set to a space separated list of subdirectories # to search for packages. # The default is "B<$ARCH common>" # i.e. to look for the architecture specific stuff # and then the architecture independent stuff. # # =item B # # Preferred path to the rsync(1) command # as though specified with the B<-R> command line option. # The default is "B". # # =item B # # Extra command line options to supply to rsync(1). # This was implemented primarily to allow supply of the B<--copy-links> # option to make link safe local copies, # robust against loss of a central server # which might ordinarily have been supplying # some non-local packages. # # =item B # # Ssh command to use, as for rsync, if a remote host is in use. # The default is "B". # # =back # # =head1 EXAMPLE CONFIGURATION FILES # # A typical machine with a small B area might say: # # perl local # mozilla local # fvwm local # # and so forth to make particular packages normally used by the machine's owner # locally installed # to provide resilience against downtime on the master NFS server. # # A modern machine with plenty of room might say: # # * local # # so that all packages are installed locally, # providing complete NFS independence from the master host. # # A minimal service machine # which normally has no NFS mounts at all # but which wants some packages installed might say: # # * nosync # perl local # rsync local # script local # # to make local installs of useful packages only, # and no installs at all of the rest. # This machine must be maintained by passing the B<-r> option to B # to use ssh(1) to access the master machine # or by setting the B environment variable # in the system environment. # # The same style of configuration would be used for a mobile machine # such as a laptop # which would not have everything local # but also would frequently not have NFS access either, # and which would not want to try # (eg if the NFS were automounted - you don't want stray symlinks firing # it up off site). # # =head1 SEE ALSO # # ssh(1), rsync(1) # # =head1 AUTHOR # # Cameron Simpson Ecs@zip.com.auE 1996 #