2008-06-30

Simulating UUID in rc.server

I've now done a reasonable amount of testing, and it seems clear that an "md5 -q" hash of the output of « pdist DISK -dump | grep -v "/dev/disk" » can be used to identify a certain disk both while the system is running and the drive is mounted, and while the system is in single-user mode (in rc.server) and the drive is not mounted.

In effect, the hash functions very similarly to the UUID method that can be used in multi-user mode in fstab.

Furthermore, I have ascertained, as is only logical, that neither /private/tmp nor /Volumes has been cleaned up at the time that rc.server is running. (It's logical because the boot drive is still mounted read-only and no other drive is yet mounted.)

Therefore, putting a time-stamped info file into /tmp that contains the hash(es) of the drive(s) containing /Volumes/Clone and /Volumes/Snapshots can be used reliably to find the appropriate drive(s) to mount in order to do the backups, and that they can be mounted in /Volumes.

2008-06-29

/dev entries

The latest bottleneck is in ascertaining the name of the disk to use for backup in /dev. The LABEL= and UUID= names don't appear to work from rc.server, and the /dev/disk? names are famously variable. For example, if someone plugged in, say, a USB drive and then plugged in the Firewire backup drive, then later removed the USB drive. Even with a predictable assignment sequence, the disk number would change.

Just as a footnote, this is something that has always driven me crazy, in UNIX, in MS-DOS, and everywhere. Why can't there be a constant mapping between a slot and a drive?

Anyway, there is a program called pdisk(1) that may work. It prints the partition table from an attached drive whether it is mounted or not, and so it will probably work from rc.server. The table it prints out is like this:

Partition map (with 512 byte blocks) on '/dev/disk1'
#: type name length base ( size )
1: Apple_partition_map Apple 63 @ 1
2: Apple_Free 262144 @ 64 (128.0M)
3: Apple_HFS Untitled 104857600 @ 262208 ( 50.0G)
4: Apple_Free 262144 @ 105119808 (128.0M)
5: Apple_HFS Untitled 480690400 @ 105381952 (229.2G)
6: Apple_Free 16 @ 586072352

Device block size=512, Number of Blocks=312581808 (149.1G)
DeviceType=0x0, DeviceId=0x0

Note that while there are various useful clues, the actual volume name doesn't appear to be present. There is an option (-f) that is supposed to cause volume names to be printed, but this has no effect in early testing.

However, the swap-search example program that I found did something interesting with the output from pdisk. They used the command "md5 -q" to create a usable checksum from all of the output of pdisk except the first line (that gives the /dev name). This allows an easy way to recognize whether the appropriate disk is present and where it is in the device tree. The command they used was swaphash=`pdisk /dev/disk${swapdisk} -dump 2>/dev/null | grep -v '/dev/disk' | md5 -q`

So one way to do this would be for the scheduled script to look for the devices by their mount names /Volumes/Clone and /Volumes/Snapshots, use mount to find the current name and partition numbers, use pdisk to compute the hash, which would be stored somewhere volatile but which would not yet be cleaned up in rc.server (?) or maybe just in /etc. Then when the system is booted, a simply loop would be run to find it, it would be mounted, and the backup would proceed.

Incidently, finding a directory like /tmp or /var/??? that gets cleaned up after rc.server is finished would be a great way to pass information to the maintenance script(s), since there's no other way that the info would be there, unless it was placed there just before booting. Note that the script can't really remove it, since the boot drive is still read-only. In fact, this implies that /tmp can be used for this purpose. To be tested...

Well, I'll be testing this all out tomorrow so will report either in a comment here or in another entry.

Here is an example of finding all disks' hashes:
for x in /dev/disk[0-9] ; do /bin/echo $x `/usr/sbin/pdisk $x -dump 2>/dev/null | /usr/bin/grep -v "/dev/disk" | /sbin/md5 -q` ; done

2008-06-28

snaps, rc.server, and firewire drives

I haven't found out much more, but here are two fairly critical facts.

Prelimaries: I have partioned a 300GB firewire drive with a 50B partition called "Clone" and a 250GB partition called "Snapshots". The plan is for Clone to contain a bootable clone of the main drive, produced by ditto once per week or so, and for Snapshots to contain the snapshots. Obviously, if I put more than 50GB on the main drive (which is a 250GB drive, by the way), this scheme won't work. In fact, I probably need a 1TB drive for this, with Clone equal in size to the main drive and the rest for snapshots. At some point, I will upgrade, but for now, I am using around 30GB on the main drive so this will be good for testing and for use for quite a while.

The first amazing fact; based on my little test with mount, firewire drives are not mounted during the time rc.server is running. So in order to do my firewire backup, I will have to mount and then unmount them. I hope that the necessary driver is available at that time. I wonder what will happen if I leave them mounted read-only...

The second fact, and I should have known this, is that when the system eventually mounts the partitions, it starts running Spotlight on them (well, they are empty so this was sort of a no-op). Once I start putting data on them, I absolutely do not want Spotlight even to see them. In fact, when I'm not actually backing up to them (from rc.server), I want them always to be mounted read-only. So I need to research these two issues: getting Spotlight to skip them, and making sure they are mounted read-only by default (possibly by mounting them read-only in rc.server?).

Hasta mañana.

2008-06-27

snaps and rc.server

Snaps is a Korn Shell backup script for our servers. It is intended to do one rsync (1)snapshot per day of the entire boot drive onto a local fireway drive. In addition, it does a periodic clone of the boot drive using ditto(1). There are a couple of unusual aspects to this script.

First, it parameterizes the archiving of snapshots in an unusual way. It keeps a week's worth of daily snapshots (all of the numbers here are parameters). Then, it uses an exponential function to decide which older snapshots to delete. It always will keep one snapshot in each integral range, so one for 2^0 days, one for 2*1 days, one for 2^2 days, one for 2^3 days, and so on up to one for 2^8 days. Then, it always keeps backups that are older than 365 days. (Remember, all of these numbers can be tweeked.) This results in a kind of S-shaped function of the frequency of preserved snapshots per unit of time. Most systems like this do something similar, but use standard calendar periods, like so many daily, so many weekly, so many monthly snapshots. I thought that the exponential function would be more general than this, so that's what "snaps" uses.

Second, and this is what I'm wrestling with at this stage of development, I want to automate this script. However, there are several server databases that cannot be "live" when a snapshot is taken, and in general, a backup is much more valuable if the system is quiescent when it is done. My idea is to set up a periodic process that will reboot the system in the "wee hours" of the morning. The snapshot script will be run early in the boot process, before things really get started in the system.

It took me quite a while to figure out how to do this because of how launchd and launchctl work. There doesn't seem to be any way to get things to happen early enough. However, a perusal of the launchctl/launchd source revealed that there is a section at just the appropriate moment when a script called "/etc/rc.server" is executed if present. This is done right after single-user mode and has much the same context as single-user mode.

I just added some lines to the end of rc.server to see what the environment is. (Rc.server's standard output is placed into /var/log/system.log.) The commands I added were /sbin/mount and /bin/ps. Here is what was reported:


Note that almost nothing is running, just launchd, launchctl, and the shell, which is running /etc/rc.server. This is a quiescent system. Also note that the boot drive is still mounted read-only, which is ideal for the purposes of making a backup.

Here is the current version of snaps:


#!/bin/ksh
# ----------------------------------------------------------------------
# snaps -- maintain a set of filesystem snapshots
# the basic idea is to make rotating backup-snapshots of sourcedir
# onto a local volume whenever called. The philosophy is to put all of
# the configuration and logging information into the backup directory,
# so that snaps requires only that path to get going. The scurve filter
# causes an s-shaped frequency of preserved snapshots, with more recent
# and fewer old snapshots.
#
# Important note: HFS+ filesystems are apparently set to ignore ownership
# for all but the boot drive. This must be disabled using the Finder's
# Get Info panel. (Is there a way to check for this programatically?)
#
# NOTE: rsync must be version 3 or better
# ----------------------------------------------------------------------
# Usage: snaps [-n] SNAPS_DIR [ROOT]

# -------shell function defs------------------------------------------

# compare the current time in secs to a list of dates
# if on return, ${snap[0]} = secs, then we need to do a backup, otherwise do nothing
# also, the old backups in rmrf need to be expunged. ante is the most recent previous
# backup, if any.
function scurve {
typeset secs age tmp x i

secs=$1 ; shift
tmp=$(perl -e "@x=sort { \$b <=> \$a } qw($*);print \"@x\",\"\\n\"")

if [[ "$tmp" == "" ]] ; then
unset snap
snap[0]=$secs
return
fi
for ante in $tmp ; do
break
done

((age=secs-ante)) # age in secs of most recent snap
if [[ age -le JOUR ]] ; then # too soon
return
fi

unset snap
unset arch
unset curr
unset rmrf
for x in $tmp ; do
((age=(secs-x)/JOUR)) # age in ticks
if [[ age -le 0 ]] ; then # too soon
print age $age secs $secs x $x
continue
fi
# take care of the current backups in "real time"
if [[ age -le CURR ]] ; then
curr="$curr${curr:+ }$x"
continue
fi
# also take care of the archival backups in "real time"
if [[ age -ge ARCH ]] ; then
arch="$arch${arch:+ }$x"
continue
fi
# now set the base of the exponential portion
((age-=CURR))
((i=1+floor(log(age)/log(BASE))))
if [[ "${snap[i]}" == "" ]] ; then # nothing in this slot yet
snap[i]=$x
elif [[ ${snap[i]} -gt $x ]] ; then # always keep the older one
rmrf="$rmrf${rmrf:+ }${snap[i]}"
snap[i]=$x
else # keep unless current
rmrf="$rmrf${rmrf:+ }$x"
fi
done
if [[ "${snap[0]}" == "" ]] ; then
snap[0]=$secs
fi
}

# errs and other log stuff all go to stderr
log(){
print -u2 -- "$where:$TO@$(date +%Y%m%d.%H%M%S) $(basename $ME .ksh): $*"
}
finish(){
if [[ -e snaps.log ]] ; then
mail -s"Snaps Status for $where:$TO" root < snaps.log
rm snaps.log
fi
exit $1
}
err(){
log "$*"
finish 1
}

nopt=0
rsyncopt(){
RSYNC_OPTS[nopt++]="$RSYNC_OPTS${RSYNC_OPTS:+ }$*"
}

# ---------------------- basic parameters --------------

# NOTE: define RSYNC to a version that is 3.0.0 or newer
RSYNC=/opt/local/bin/rsync

# these are for error message purposes (see functions log & err)
ME=$0
where=$(hostname)

# limit path to /bin and /usr/bin except we need
PATH=/bin:/usr/bin

# ------------- args, file locations ----------------------------

case "$1" in
"-n" ) now=print ; dry="-n" ; shift ;;
* ) now= ; dry= ;;
esac

TO=$1

if [[ "$TO" == "" ]] ; then
err "Usage: snaps [-n] SNAPS_DIR]"
fi

# make sure we're running as root so we can start logging
if [[ `id -u` != 0 ]] ; then err "Not root" ; fi

if [[ ! -d $TO ]] ; then
err "No such directory $TO"
fi
eval `stat -s $TO`
if [[ $st_uid -ne 0 || $(($st_mode&0777)) -ne $((0755)) ]] ; then
err "$TO not mode 755 directory owned by root $st_uid $st_mode $(($st_mode&0777)) 0755"
fi

cd $TO

# set up errors from this point to be redirected to the log except for dry runs
# we do one log per backup and we store it in the snapshot folder as a record
# of that snapshot

if [[ "$now" == "" ]] ; then
if ! exec 2> snaps.log ; then
err "failed to write in $TO -- read only volume?"
fi
fi

log "Begin $dry"

# -------------- rsync parameters -------------
rsyncopt -vq # verbose error messages
rsyncopt -a # archive mode: -rlptgoD
rsyncopt -x # do not cross filesystem boundaries
rsyncopt --protect-args
rsyncopt --delete-excluded # implies --delete
rsyncopt -A # --acls
rsyncopt -X # --xattrs

# the makers of carbon copy cloner also recommend these options which are
# not available in the macports version of the program:
# rsyncopt --fileflags
# rsyncopt --force-change

# ------------ do some more checking -----------------

# NOTE: this needs to check for "Capabilities" <<<<<<<<<<<<<<<<<<<<<<
# insist on v. 3.X for working link-dest and xattrs
# if and when v. 4.X comes out, fix the script
case "$($RSYNC --version)" in
*'version '[012]'.'* ) err "$RSYNC is older than version 3.X" ;;
*'version '[456789]'.'* ) err "$RSYNC is newer than version 3.X" ;;
esac

# --------- the snapshots subdirectory ---------------

DD=$TO/snapshots
if [[ ! -d $DD ]] ; then
err "No such directory: $DD"
fi
eval `stat -s $TO`
if [[ $st_uid -ne 0 || $(($st_mode&0777)) -ne $((0755)) ]] ; then
err "$DD must be an rwx directory owned by root"
fi

# --------- configuration files -----------------
# they can be empty, but they must be uid0 and mode 0644

for x in config filter ; do
if [[ ! -f $TO/snaps.$x ]] ; then
err "No such file: $TO/snaps.$x"
fi
eval `stat -s $TO/snaps.$x`
if [[ $st_uid -ne 0 || $(($st_mode&0777)) -ne $((0644)) ]] ; then
err "$TO/snaps.$x not mode 0644 and owned by root"
fi
done

# ---------- use filter file if there is one -------
if [[ ! -s $TO/snaps.filter ]] ; then
rsyncopt "--cvs-exclude"
else
rsyncopt "--filter=. $TO/snaps.filter"
fi

# -----------------everything looks ok, let's get started--------------

# set defaults
ROOT="/"
VERSION=1
CURR=7
ARCH=731
JOUR=86400
BASE=2

# get overrides and other config info
# the only thing legal in this file is variable definitions
# of a few numeric or filepath parameters. to do comments, simply start
# the line with "#" or the word "rem" or "comment".
exec < snaps.config
while read x path ; do
for y in $path ; do
break
done
case $x in
"" ) continue ;;
ROOT )
ROOT="$path"
continue
;;
VERSION|CURR|ARCH|JOUR|BASE )
if [[ "$y" == "" || "$y" == *[^0-9.]* || "$x" != "$path" ]] ; then
err "Bad assignment in snaps.config line: \"$x\" \"$path\""
fi
eval "$x=$y"
continue
;;
comment|COMMENT|rem|REM ) continue ;;
"#"* ) continue ;;
* ) err "Unknown parameter in snaps.config line: \"$x\" \"$path\""
esac
done

# what time is it?
secs=$(date +%s)

# see if there is any work to do
unset snap
unset curr
unset arch
unset rmrf
unset ante

scurve $secs `ls snapshots`
if [[ ${snap[0]:-NIL} -ne $secs ]] ; then
log "Too soon"
exit 0
fi

# for log
df $TO

# remove unwanted snapshots if any
for x in $rmrf ; do
log "Unlinking $x"
$now rm -rf snapshots/$x
done

# if we crashed before, get rid of the remains
for x in *.partial ; do
if [[ -d $x ]] ; then
print "Unlinking $x for $where:$TO on `date`" >> snaps.log
$now rm -rf $x
fi
done

# is there a previous version to use with link-dest?
if [[ "$ante" != "" ]] ; then
rsyncopt "--link-dest=$TO/snapshots/$ante${ROOT:+/}$ROOT"
fi

# rsync from the system into the new snapshot
log "$RSYNC $dry "${RSYNC_OPTS[@]}" "$ROOT/" $TO/$secs.partial"
$RSYNC $dry "${RSYNC_OPTS[@]}" "$ROOT/" "$TO/$secs.partial"

# move the snapshot into place
$now mv "$secs.partial" snapshots/$secs

# update the mtime of the snapshot to reflect the snapshot time
$now touch snapshots/$secs

# and thats it.

df $TO

log "Completed $dry"

$now ln snaps.log snapshots/$secs

finish 0

What is this?

I spend a lot of time writing various kinds of scripts to support activities in our lab at UC Davis. They range from system administration scripts to scripts used to set up or analyse data from experiments to "helper" scripts for formatting various kinds of documents. Sometimes it helps to write about the scripting process, which I have generally done in the form of notes to myself, a kind of brainstorming and autodocumentation. I decided that it might be useful to do it online instead. That way, someone else might find something useful or something to avoid, and I might get a useful suggestion or two about it.

So in that spirit, I'm going to kick off this blog with various projects that are underway.

About Me

My photo
Ignavis semper feriæ sunt.