Ongoing brainstorming/documentation/repository of scripts and other utility programs I've written or am writing for our lab. Note that there are no guarantees that what works in our lab will work in yours. Also, although I'm trying to accurate and as thorough as possible, I may not take the time to go back and correct things that I used to think were correct but no longer are.
2008-11-15
Note on using webdav idisk for experiment data
2008-11-13
Combining image and rsync backups
- Unless you want to bring the system down to single user mode every night, you are still going to have to deal with open database files during the rsync.
- Assuming that those files have been dealth with, would it be faster just to do the image dump every night?
- Is the target system more likely to be bootable in the even of a catastrophe mid-copy if rsync is used?
- One of the biggest advantages of using rsync is the ability to do snapshots. Clearly, the original image could be used as the starting point for nightly snapshots, but then the image itself would never be updated. Is there some way for the root hierarchy to be the most recent snapshot, with previous versions stored elsewhere? (See below)
- Before any backup, go through the process of dumping all system databases.
- For the first (labor intensive) backup, use asr to make a complete copy of the base system. The system should be as quiesent as possible for this, and the system database dumps should contain the current database contents. The backup volume should be considerably larger than the working volume.
- Dump the databases and use rsync to update the smaller backup volume. Be careful to exclude the /snapshots directory on the backup volume; this should be untouched by this run of rsync.
- Then use rsync again to create a new snapshot of the non-snapshot regions of the backup volume (there is no need for an additional database dump). This will be stored in /snapshots on that volume (and so /snapshots will again be excluded).
2008-09-12
OS/X single-user-mode backups
- The setup-and-reboot script, which would search for the target volumes, decide which one to write to next, and if there is one, to store that information in /tmp/rc.autoclone or whatever. This script will change the name of the root volume just before rebooting under plan 2.
- The clone script, which would expect information in /tmp/autoclone that if it checked out will control the clone operation.
- The cleanup script, to be run very early in the boot process, to change the name of the root volume back to its normal name. In plan 1, the name will contain a time stamp, in plan 2, it will be an ordinary name like "Lab 13".
- Check for /tmp/rc.autoclone
- Check that the timeout interval has not passed (e.g., 5 minutes)
- Check for /tmp/mnt
- Get info on all current drives
- Search for the drive indicated in /tmp/rc.autoclone
- Mount the drive on /tmp/mnt and compare its version of rc.autoclone
- Unmount the target drive
- Perform the clone operation
- Done
- Get the current / volume name
- If it needs to be changed, change it
- Do general checking to prevent being called at the wrong time (e.g., too soon)
- Make sure that at least one target volume is available
- Look through all possible target volumes to find the one with the oldest timestamp.
- Compute its ID string
- Create an rc.autoboot containing the ID string
- Write the rc.autoboot into the root directory of the target volume
- Reboot
2008-08-08
An example experimental project
2008-08-03
Multiple superlab stimulus folders
2008-07-21
Converting text to graphic files for Superlab
ppmmake lightblue 640 480 \
| ppmlabel -x $((320-(5*${#1}))) -y 240 -size 10 -background lightblue -color black -text "$1" \
| pnmtojpeg > $2 2>/dev/null
}
2008-07-18
How to set up an sl4am project
A Useful Experimental Data Hierarchy
This is a scheme for storing experimental data, primarily with SuperLab, that should be easy to setup, easy to use, and easy to automate.
At several specific points throughout the hierarchy there are rc files: rc.sl4am, rc.proj, rc.subj, and rc.exp. These can contain arbitrary ksh code and can change the operation of sl4am, but are intended to customize the operation of sl4am by changing specific parameters or by altering the setup, runtime, and/or cleanup phases of a SuperLab scenario.
The SL4AM subfolder within the SuperLab Experiments folder is the default sl4am home folder. If an rc.sl4am file is present, it can redefine the variable SL4AMHOME in order to use a different home folder, for example, in a shared location on the local machine, like "/Users/Shared/Documents/SuperLab Experiments/SL4AM", or on a remote location, such as "/Volumes/groups.ourlab/Documents/SuperLab Experiments/SL4AM". If there is a separate root for the status flags and datafiles, that can be defined in rc.sl4am as "DATAROOT" (see below). If DATAROOT is defined, it should give the path to a folder that will be organized in parallel to the SL4AM folder.
Note that spaces in folder and file names should avoided if possible below SL4AM. I'm trying to make the script immune to problems resulting from spaces in filenames, but it's safer (and much wiser) to avoid them.
Project roots are top-level subfolders in SL4AM. There can also be a top-level subfolder in SL4AM called Archival that is intended to store completed or inactive projects.
Except for some code at the beginning to select a project, sl4am is concerned only with the world within a single project root, and in fact, it uses cd to go there as soon as possible so that most paths within a project are relative to the project's root. While running SuperLab, sl4am changes temporarily into the folder of the specified scenario (see below). At the top level of a project root, there must exist a file called rc.proj that is sourced when sl4am starts a session in that project. A subfolder X of SL4AMROOT is a project root iff X/rc.proj exists.
Each project contains a subject hierarchy. The top level is a dot-separated list of population.group folders, for example, Healthy.Elderly, Healthy.Young, Schiz.Elderly, Schiz.Young. Below each population.group folder is one or more condition.group folders, such as Set1.VisualFirst, Set1.AuditoryFirst, Set2.VisualFirst, Set2.AuditoryFirst. Below each condition-group folder is one or more numeric subject folders, for example 1, 2, 3. There is one subject folder for each subject to be tested in the project. Each subject folder must contain a flag file. It may be desirable for sl4am to create new subject folders automatically as needed; in any case, the names of these folders are simply numbers starting at 1. There also needs to be a way to insure that sl4am will test the first subject in each condition before starting the numerically next subjects, and so on.
Note that even though some labs never test any population group other than college students, both group levels are required. The best way to handle this situation is to use two population group identifiers, one named "Try" and the other something like "YN" (young normal). The Try group is for testing the experimental setup, while the YN group is a reasonable label for college students. If at some point you need to add another population group, it will be very easy to do it. The presence of the "YN" level won't interfere with anything, and the "Try" pseudogroup is very useful for development and for training RAs.
The flag file is a simple advisory access-control mechanism. When an experiment is first set up, each slot's flag file is named "flag.free", indicating that any computer can run that subject slot. The file is renamed to "flag.user@en0", where "user" is the current user and en0 identifies the current host. (The figure says "fern", which is the name of one of our computers; this method is too variable.) This "checks out" the subject slot to the named individual. The parameter "en0" is set to the ethernet address of en0 (ifconfig en0 | grep ether | read junque ether junque) in order to identify the machine in a somewhat unambiguous way. An individual may relinquish a subject slot by renaming the flag file back to "flag.free", but the presence of datafiles will cause a multi-experiment sequence to pick up where it left off before. When a subject slot is complete and all data has been saved, the flag file must be renamed to "flag.completed".
Note that if DATAROOT is defined, then sl4am will search there for flag.free slots, change to flag.user@en0 there, and will upload data there. The structure under DATAROOT is identical to the SL4AMHOME hierarchy, but it need not have any Shared folder, rc files, or scenarioes or stimuli. In the event that subjects must be tested offline, one or more subject slots should be checked out in advance. When sl4am starts up, it will sweep all experiments looking for the flag "SYNC" in a subject-number folder. If it finds any, and if DATAROOT is accessible, it will synchronize the parallel structure there by uploading any data not already present, and renaming the flag file if necessary. Any time sl4am tries to upload data or change the flag file but DATAROOT is inaccessible, SYNC is created. After a successful upload, it is removed.
There also must be an rc.subj file in a subject-number folder, primarily designed to control running multi-experiment projects (for example, the order in which to run the experiments). This can be a zero-length file.
For each experiment, there is an experiment root under the subject-number folder. This folder contains all that is needed to run one SuperLab experiment: the scenario (.sl4) file; all stimuli needed (these will generally be links elsewhere, or in the case of all-text experiments, missing altogether); and the datafile (.txt) once the experiment has been run. There is also a mandatory rc.exp file, primarily intended to help customizing the SuperLab run or the datafile processing afterwards. For example, this could give the subject some feedback about his performance. Sl4am cd's into the experiment root before running rc.exp and SuperLab.
Under each project folder, there can be an optional Shared subfolder. This contains all shared stimuli and/or sl4 files. For convenience, the setup script will install a Shared symbolic link in each experiment root that points back to the project-level Shared folder (if it exists). To link to it from a stimulus folder in an experiment root under a subject file, just use
ln -s ../Shared/SomeFolder/somefile.xxx stimset/somename.xxx
To link a scenario file in the experiment root to one in the Shared folder, use
ln -s ./Shared/some-exp.sl4 some-exp.sl4
It is possible to fully populate this tree before beginning to run, but it is equally possible to use the rc scripts to populate fill things out at run time.
UPDATE
It really isn't hard to link relatively back to the Shared folder without the klutzy locate Shared link. Here's how to do it. You create a dummy tree all the way down to the stim/stimset folder inside an experiment. Also, create a Shared folder with a stimset/xxx.png in it. Then cd down there and do "ls ..", "ls ../.." and so on until you get to (e.g.) "ls ../../../../../../Shared/stimset". The test it with ln -s ../../../../../../Shared/stimset/xxx.png yyy.png" or whatever. Once you've done this and gotten it to work, just make sure that your script does two things: use the right number of dotdots, and actually cd into the stimset folder to do the linkage:
(cd downpath; ln -s uppath/Shared/stimset/file.suff link.suff)
Note that assuming the script is running in the project root, downpath will be like "pgrp/cgrp/subj/exp/stim/stimset", and uppath will be like "../../../../../..". As for why this is worth doing instead of just using absolute links, it's to allow experiments to be installed using simple methods like tar. With relative links, no adjustment is required, with absolute links, basically there would need to be a script run to adjust links after installation in new locations.
2008-07-15
Subject checkout on shared volume
2008-07-09
SuperLabAutoMator: superlab + automator
- name=xxx -- the subject ID to use (default = null)
- group=xxx -- the subject's group (default = null)
- scenario=ppp -- default is "scenario.sl4" in the experiment subfolder
- logfile=ppp -- default is "logfile.txt" in the experiment subfolder
- fifofile=ppp -- use as Superlab's logfile; filtered data should be written to $logfile by midscript
- prescript() -- set up to run
- midscript() -- interact with Superlab while running
- postscript() -- run after midscript and Superlab have finished
2008-07-01
Sending email to root
Where to put maintenance scripts
2008-06-30
Simulating UUID in rc.server
2008-06-29
/dev entries
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
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.
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
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
#!/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