# This file is meant to be sourced by runme commands # vim:set filetype=sh: # $Id: //depot/ops/corp/goobian/files/var/lib/getupdates/funcs#3 $ # --- bootstrap snip start --- # changeset log function, separate from log() in getupdates log () { DATE=`date "+%Y/%m/%d %H:%M:%S ($$)"` echo "$DATE LOG: $1" } # changeset die function, separate from die() in getupdates die () { DATE=`date "+%Y/%m/%d %H:%M:%S ($$)"` echo "$DATE DIE: $1" exit 1 } ##################################################################### # Default variables that we can override in /etc/sysconfig/getupdates ##################################################################### # Who we ping to decide if we're up or not # The idea is not to generate stderr output (i.e. a cron mail) if we're still # down BASECHECKHOST=host.domain.tld # this is what we base shortcuts on. Again, this is to save space in the kernel # parameters, since they're limited to 255 chars DOMAIN=sub.domain.tld # We use short, sucky names because they can be fed from the kernel command # line, and it has a limit of 255 chars # TARGD is a common root added to NFSUPDPATH and HTTPUPDPATH TARGD="/goobian/targets" # Which snapshot of the sub-target we want current by default SNAP="current" # Base access to update info over NFS (TARGD is appended) # It is important to split this up and mount $NFSHOST:/$NFSUPDPATH and not # $NFSHOST:/$NFSUPDPATH/$TARGD because some NFS implementations (like ontap # nfs v4 virtual mounts) will not let you mount more than 1 level deep NFSHOST="software.nfs" NFSUPDPATH="/software/" # Base access to update info over HTTP (TARGD is appended) HTTPUPDPATH="http://apt/" # Where we write output STATBASE="/auto/clientinfo" # This is mostly for install time status reporting (when /auto automount # doesn't work) NFSSTATMNT="clientinfo.nfs:/clientinfo" # Do we show changes instead of applying them? FAKEUPDATE="no" # Base "work" directory where we store our state and tmp files WORKDIR="/var/lib/getupdates" # we set a default device in case get-ip-info isn't there, and the user can # override in sysconfig if needed DEVICE=eth0 # Get a new device if the script is available. The script also gives us IP eval `get-ip-info 2>/dev/null` # --- if [ -f /etc/sysconfig/getupdates ]; then . /etc/sysconfig/getupdates else die "can't read /etc/sysconfig/getupdates" fi # goobian variables specific to each machine, usually to control changeset # behaviour (i.e. variables that are not related to getupdates itself) # Keep last changed date (i.e. conditional touch) test -f /etc/sysconfig/goobian || touch /etc/sysconfig/goobian . /etc/sysconfig/goobian #################### # Other runtime vars #################### if [ -z "${TARGET:-}" ]; then die "target= not given on kernel command line, fatal" fi # Used for logging in getupdates, but could be used by changesets too. # We allow the user to override the MAC address in sysconfig if [ -z "${MAC:-}" ]; then MAC=`ifconfig $DEVICE | head -1 | awk '{print $5}'` fi # We whould have gotten an IP from get-ip-info, but in case it didn't run, and # it didn't get set in sysconfig if [ -z "${IP:-}" ]; then IP=`ifconfig $DEVICE | head -2 | tail -1 | sed -e "s/[^:]*://" -e "s/ .*//"` || true fi if [ ! -d $WORKDIR/tmp ]; then mkdir -p $WORKDIR/tmp || die "Can't mkdir $WORKDIR/tmp" fi if [ ! -d $WORKDIR/work ]; then mkdir -p $WORKDIR/work || die "Can't mkdir $WORKDIR/work" fi if [ ! -d $WORKDIR/state ]; then mkdir -p $WORKDIR/state || die "Can't mkdir $WORKDIR/state" fi INSTALLEDFILES=$WORKDIR/state/installedfiles if [ ! -f $INSTALLEDFILES ]; then touch $INSTALLEDFILES || die "Can't create $INSTALLEDFILES" fi # Default protocol is NFS [ -z "${PROTOCOL:-}" ] && PROTOCOL="nfs" LEADGOLD=`echo $TARGET | sed 's/^.*\(....\)$/\1/'` [ -z ${LEADGOLD:-} ] && die "DIE: couldn't compute LEADGOLD from /etc/sysconfig/getupdates (TARGET is $TARGET)" MACHTYPE=`echo $TARGET | sed 's/^\(.*\)....$/\1/'` [ -z ${MACHTYPE:-} ] && die "DIE: couldn't compute MACHTYPE from /etc/sysconfig/getupdates (TARGET is $TARGET)" # We use hostname -f to make sure that the fully qualified hostname works HOSTNAME=`hostname -f 2>/dev/null | sed 's/\.[^.]*\.[^.]*$//'` || true if [ -z "${HOSTNAME:-}" ]; then HOSTNAME=`hostname 2>/dev/null` || true log "WARNING: hostname on $HOSTNAME/$IP is a non qualified hostname or is not correctly listed in /etc/hosts, please fix" fi if [ -z "${HOSTNAME:-}" ]; then HOSTNAME="unknown" log "WARNING: Couldn't get hostname on $IP, please fix" fi if [ -z "${SHOWROTATED:-}" ]; then SHOWROTATED="^Rotated" fi epocdate2str () { date -d "1970-01-01 $1 sec UTC" "+%Y/%m/%d %H:%M:%S" } pinghost () { HOST=$1 FPING=fping if ! fping -q localhost 2>/dev/null; then log "$FPING localhost failed, skipping fping check for $HOST" return 0 fi # First, we make sure that we have basic connectivity up. We don't ping # our gateway (that's not good enough), we want a further away host that # we consider a good indication of general connectivity # Try 10 times with 5 seconds in between $FPING -B5.0 -r 10 $BASECHECKHOST &>/dev/null; ret=$? if [ $ret -ne 0 ]; then # We don't die and output on stderr, we want to avoid the cron mail log "FATAL: networking seems to be down: can't reach $BASECHECKHOST" log "`fping -A -c 3 $BASECHECKHOST 2>&1`" exit 1 fi # Then make sure host we pull from works too $FPING -B5.0 -r 10 $HOST &>/dev/null; ret=$? if [ $ret -ne 0 ]; then log "Can't fing $HOST, fping output is:" log "`fping -A -c 3 $HOST 2>&1`" fi return $ret } HTTPHOST=`echo $HTTPUPDPATH | sed 's/http:\/\/\([^/]*\)\/.*/\1/'` NFSSTATUSHOST=`echo $NFSSTATMNT | sed 's/:.*//'` if [ "${PROTOCOL:-}" = nfs ]; then if ! pinghost $NFSHOST; then # Only generate stderr and a cron error mail if we aren't NFS logging if [ "${LOG:-}" != /dev/null ]; then log "FATAL: PING: Can't ping $NFSHOST (was configured for NFS)" die else die "PING: Can't ping $NFSHOST (was configured for NFS)" fi fi UPDPATH="$NFSUPDPATH/$TARGD/$TARGET/$SNAP" elif [ "${PROTOCOL:-}" = http ]; then if ! pinghost $HTTPHOST; then # Only generate stderr and a cron error mail if we aren't NFS logging if [ "${LOG:-}" != /dev/null ]; then log "FATAL: PING: Can't ping $HTTPHOST (was configured for HTTP)" die else die "PING: Can't ping $HTTPHOST (was configured for HTTP)" fi fi UPDPATH="$HTTPUPDPATH/$TARGD/$TARGET/$SNAP" else die "Fatal: No support for protocol $PROTOCOL" fi # Getfile will give output on STDERR about files it can't receive and why, # unless you call it with --nowarn # --quiet will not show data about files retreived # The function will return 0 for success and 1 for file retrieval failure getfile () { warn() { echo "$#" >&2 } if [ "${1:-}" = --nowarn ]; then shift warn () { : } fi QUIET="" if [ "${1:-}" = --quiet ]; then QUIET=-q shift fi DESTFILE=`basename "$1"` FILE="$1" if [ "${PROTOCOL:-}" = nfs ]; then if [ -d $UPDPATH ]; then if [ ! -f $UPDPATH/"$1" ]; then warn "$UPDPATH/$1 not there" return 1 fi ln -snf $UPDPATH/"$1" $WORKDIR/tmp/ else die "Protocol NFS configured, but $UPDPATH not found" fi elif [ "${PROTOCOL:-}" = http ]; then /bin/rm "$DESTFILE" &>/dev/null || true if ! wget $QUIET $UPDPATH/"$1" 2>&1; then warn "wget $UPDPATH/$1 failed ($?)" return 1 fi else die "Unknown protocol ${PROTOCOL:-[undefined]}" fi return 0 } # --- bootstrap snip end ---- removefiles () { for file in "$@" do if [ -L "$file" ]; then echo "REMOVEFILE: $file (symlink)" /bin/rm "$file" elif [ -f "$file" ]; then echo "REMOVEFILE: $file" chattr -i "$file" /bin/rm "$file" fi done } rotatefile () { if [ "$#" -ne 1 ]; then echo "DIE: rotatefile file (called with $# args: $@)" >&2 exit 199 fi if [ -L "$1" ]; then echo "ROTATEFILE: $1 is a link, moving to $1.oldlink" /bin/mv -f "$1"{,.oldlink} else savelog -c 9 -l "$1".orig | grep -v $SHOWROTATED || true test -z "$1".orig.0 && /bin/rm "$1".orig.0 # In many cases, we will try to rotate out an immutable file. # Account for that chattr -i "$1" /bin/mv "$1"{,.orig} fi } # you can call this function one of three ways: # installlink /etc hosts.foo hosts # installlink hosts.foo /etc/hosts # installlink /usr/local/bin/mybin /bin/ installlink () { if [ "$#" -eq 3 ]; then DIR="$1" shift elif [ "$#" -eq 2 ]; then DIR="." else echo "DIE: installlink [dir] src dest (called with $# args: $@)" >&2 return 299 fi SRC="$1" DEST="$2" # allow installink /bin/foo /lib test -d "$DEST" && DEST="$DEST/`basename $SRC`" pushd "$DIR" >/dev/null if [ -e "$DEST" ] ; then chattr -i "$DEST" if [ ! -L "$DEST" ]; then echo "INSTALLLINK: $DEST exists but isn't a link, rotating and linking $SRC to $DEST" rotatefile "$DEST" ln -s "$SRC" "$DEST" else echo "INSTALLLINK: $DEST exists and is a link, leaving alone" fi else if [ -L "$DEST" ] ; then echo "INSTALLLINK: $DEST is dangling. Force-linking $SRC to $DEST" echo "INSTALLLINK: $DEST was pointing to `readlink $DEST`" || true ln -snf "$SRC" "$DEST" else echo "INSTALLLINK: $DEST doesn't exist. Linking $SRC to $DEST" ln -s "$SRC" "$DEST" fi fi popd >/dev/null } chattr () { if [ "$#" -ne 2 ]; then echo "DIE: chattr [-|+]flags file (called with $# args: $@)" >&2 exit 399 fi if [ -L "$2" ]; then echo "CHATTR: Not running chattr $1 $2, $2 is a symlink" return 0 fi /usr/bin/chattr $1 "$2" return $? } lsattr () { if [ "$#" -ne 1 ]; then echo "DIE: chattr file (called with $# args: $@)" >&2 exit 499 fi if [ -L "$1" ]; then return 0 fi /usr/bin/lsattr "$1" } # return 0 (true) if the file on disk doesn't match the stored md5 # return 1 (false) if the file on disk matches the stored md5 # return -1 (false) if the file on disk doesn't have a stored md5 checkinstalledfile () { file="$1" test -f "$file" || die "checkinstalledfile called with $file that doesn't exist" # match will get all the matching lines in the file, we cut out the last one # later so as not to lose the return value of grep # Aaah, the joys of shell, IFS makes sure that newlines are kept in match IFS="" match=`egrep "^$file\>" $INSTALLEDFILES` if [ $? -ne 0 ]; then return 255 else savedmd5=`echo $match | tail -1 | awk '{ print $2 }'` if [ `md5sum "$file" | awk '{ print $1 }'` != $savedmd5 ]; then return 0 else return 1 fi fi } # So, here's what we do: # 1) If the destination doesn't exist, the file is installed as immutable # and recorded as such in INSTALLEDFILES # 2) If the source and destination are identical (and aren't symlinks), nothing # happens # 3) If the destination exists but the md5 doesn't match what we saved in # INSTALLEDFILES, don't install the new file (but save the new md5) # 4) If the destination has no saved md5, rotate it out once, install the new # file and save the md5 # 5) If the destination exists and the md5 matches what we saved in # INSTALLEDFILES, install the new file and save the new md5 installfile () { MUT=N ROT=N OVR=N CHMOD= CHOWN= # Call with --chmod 0400 for instance if [ "$1" = --chmod ]; then shift CHMOD=$1 shift fi # Call with --chown root:slocate for instance if [ "$1" = --chown ]; then shift CHOWN=$1 shift fi if [ "$1" = -r ]; then ROT=Y shift fi if [ "$1" = -o ]; then OVR=Y shift fi if [ "$1" = -m ]; then MUT=Y shift fi if [ "$#" -ne 1 ]; then echo "DIE: installfile /path/to/file (called with \"$*\")" >&2 return 399 fi FILE="$1" # The loop allows FILE to be set to '*' for srcfile in files/$FILE do # If we expand '*' we can't use $FILE and need to use the expansion targetfile=${srcfile#files/} if [ ! -e "$targetfile" ]; then echo "FILEINSTALL YES: $targetfile newly installed" else if ! test -L "$srcfile" && ! test -L "$targetfile" && cmp -s "$srcfile" "$targetfile"; then echo "FILEINSTALL NO: Not installing $targetfile, identical to destination" # We delete the source so that we can survey if source files # were left behind later /bin/rm "$srcfile" # skip the section below that installs the new file continue elif checkinstalledfile $targetfile; then echo "FILEINSTALL NO (WARN): Not rotating/overwriting user modified file $targetfile (md5 doesn't match)" # We delete the source so that we can survey if source # files were left behind later /bin/rm "$srcfile" # skip the section below that installs the new file continue elif [ 255 -eq 255 ]; then # we replace a file on disk with no md5 chattr -i "$targetfile" if [ $OVR = N ]; then echo "FILEINSTALL YES: Very first install of $targetfile (rotating existing file)" rotatefile "$targetfile" else echo "FILEINSTALL YES: Very first install of $targetfile (replacing existing file)" # delete the file so that cp doesn't truncate a # file in use (bad for a shell script) /bin/rm "$targetfile" fi elif [ $? -eq 1 ]; then # file on disk matches md5 if [ $ROT = Y ]; then echo "FILEINSTALL YES: $targetfile installed after rotating old file" chattr -i "$targetfile" rotatefile "$targetfile" else echo "FILEINSTALL YES: $targetfile installed by overwriting old file" chattr -i "$targetfile" fi else die "Unknown return value $? for checkinstalledfile $targetfile" fi fi # remember md5sum of file we are trying to install # we only match the last entry with tail -1 on query md5sum $srcfile | sed "s#files/##" | awk "{ print \$2 \" \" \$1 \" \" `date +%s` }" >> $INSTALLEDFILES # updating getupdates causes the script to run itself while it is being # modified, so we unlink the destination first so that bash can run the # unlinked version while we install the new one (this is done by mv) tmptarget=`mktemp "$targetfile.XXXXXX"` # first move to the right partition (we delete the source so that we can # survey if source files were left behind later) /bin/mv "$srcfile" "$tmptarget" # then do an atomic overwrite /bin/mv -f "$tmptarget" "$targetfile" [ ! -z "$CHMOD" ] && chmod $CHMOD "$targetfile" [ ! -z "$CHOWN" ] && chown $CHOWN "$targetfile" # Hint that file is automatically maintained, unless mutable was asked if [ $MUT = N ]; then chattr +i "$targetfile" fi done } if test -x /bin/rpm; then rpm () { TMPFILE=`mktemp /tmp/rpm$$.XXXXXX` || return 1 ( ( set +e; /bin/rpm "$@" 3>&2 2>&1 1>&3; echo $? > $TMPFILE ) | sed -e "s/\(warning:.*NOKEY, key ID.*\)/ok->\1/" -e "s/\(warning:.*\.rpmorig\)/ok->\1/" -e "s/\(warning:.*\.rpm[ns][ea][wv]\)/ok->\1/" -e "s/\(warning:.*skipping V4 signature\)/ok->\1/" ) 3>&2 2>&1 1>&3 RET=`cat $TMPFILE` /bin/rm $TMPFILE return $RET } fi apt-get () { TMPFILE=`mktemp /tmp/apt$$_a.XXXXXX` || return 1 TMPFILE2=`mktemp /tmp/apt$$_b.XXXXXX` || return 1 ( ( set +e; /usr/bin/apt-get -q --trivial-only "$@" 3>&2 2>&1 1>&3; echo $? > $TMPFILE ) | sed -e "s/\(warning:.*NOKEY, key ID.*\)/ok->\1/" -e "s/\(warning:.*\.rpmorig\)/ok->\1/" -e "s/\(warning:.*\.rpm[ns][ea][wv]\)/ok->\1/" -e "s/\(warning:.*skipping V4 signature\)/ok->\1/" ) 3>&2 2>&1 1>&3 | tee $TMPFILE2 RET=`cat $TMPFILE` if [ "$RET" != 0 ]; then cat $TMPFILE2 >&2 fi /bin/rm $TMPFILE $TMPFILE2 return $RET } apt-get-force () { TMPFILE=`mktemp /tmp/apt$$_a.XXXXXX` || return 1 TMPFILE2=`mktemp /tmp/apt$$_b.XXXXXX` || return 1 ( ( set +e; /usr/bin/apt-get -q -y "$@" 3>&2 2>&1 1>&3; echo $? > $TMPFILE ) | sed -e "s/\(warning:.*NOKEY, key ID.*\)/ok->\1/" -e "s/\(warning:.*\.rpm[ns][ea][wv]\)/ok->\1/" -e "s/\(warning:.*\.rpmorig\)/ok->\1/" -e "s/\(warning:.*skipping V4 signature\)/ok->\1/" ) 3>&2 2>&1 1>&3 | tee $TMPFILE2 RET=`cat $TMPFILE` if [ "$RET" != 0 ]; then cat $TMPFILE2 >&2 fi /bin/rm $TMPFILE $TMPFILE2 return $RET } # # example usage: # if apt-get-install-check pkgA pgkB pkgC-; then apt-get-force install pkgÀ pkgB pkgC-; fi # The above would force install of pkgA and pkgB and removal of pkgC # iff no packages other than A,B, and C would be affected. Append "-" # to packages names to be removed. Function accepts version numbers # prefaced by "=" but does not verify that apt would affect that # particular version (it merely strips them off). # apt-get-install-check () { TMPFILE=`mktemp /tmp/apt$$_a.XXXXXX` || return 1 TMPFILE2=`mktemp /tmp/apt$$_b.XXXXXX` || return 1 ( ( set +e; /usr/bin/apt-get -s install "$@" 3>&2 2>&1 1>&3; echo $? > $TMPFILE ) | sed -e "s/\(warning:.*NOKEY, key ID.*\)/ok->\1/" -e "s/\(warning:.*\.rpmorig\)/ok->\1/" -e "s/\(warning:.*\.rpm[ns][ea][wv]\)/ok->\1/" -e "s/\(warning:.*skippingV4 signature\)/ok->\1/" ) 3>&2 2>&1 1>&3 | tee $TMPFILE2 RET=`cat $TMPFILE` if [ "$RET" != 0 ]; then cat $TMPFILE2 >&2 /bin/rm $TMPFILE $TMPFILE2 return $RET fi installregex="^$" removeregex="^$" for arg in "$@" do lastchar=${arg:(-1):1} if [ "$lastchar" == - ]; then argnodash=${arg%?} argnoversion=${argnodash%%[=]*} removeregex=$removeregex"|^Remv $argnoversion " else argnoversion=${arg%%[=]*} installregex=$installregex"|^Inst $argnoversion " fi done if egrep "^Inst " $TMPFILE2 | egrep -qv "$installregex" || egrep "^Remv " $TMPFILE2 | egrep -qv "$removeregex"; then cat $TMPFILE2 >&2 echo "apt-get-install-check: Files other than $@ would be installed or removed, failing sanity check" >&2 RET=599 else RET=0 fi /bin/rm $TMPFILE $TMPFILE2 return $RET }