Replace external waitfor script with a function
[autocluster.git] / autocluster
index 5b0e4f53dd38c2c6847c37d679fe9205670a1914..3c8219f150cc338d394da426d31690e207a94449 100755 (executable)
 # There are better ways of doing this but not if you still want to be
 # able to run straight out of a git tree.  :-)
 if [ -f "$0" ]; then
-    installdir="`dirname \"$0\"`"
+    autocluster="$0"
 else
-    autocluster=`which $0`
-    installdir="`dirname \"$autocluster\"`"
+    autocluster=$(which "$0")
 fi
+if [ -L "$autocluster" ] ; then
+    autocluster=$(readlink "$autocluster")
+fi
+installdir=$(dirname "$autocluster")
 ##END-INSTALLDIR-MAGIC##
 
 ####################
@@ -36,16 +39,8 @@ usage ()
 Usage: autocluster [OPTION] ... <COMMAND>
   options:
      -c <file>                   specify config file (default is "config")
-EOF
-
-    releases=$(list_releases)
-
-    usage_smart_display \
-       defconf "WITH_RELEASE" "" \
-       "<string>" "specify preset options for a release using a version string.  Possible values are: ${releases}."
-
-cat <<EOF
-     -e <expr>                   execute <expr> and exit (advanced debugging)
+     -e <expr>                   execute <expr> and exit
+     -E <expr>                   execute <expr> and continue
      -x                          enable script debugging
      --dump                      dump config settings and exit
 
@@ -57,10 +52,14 @@ EOF
     cat <<EOF
 
   commands:
+     base [ create | boot ] ...
+
+     cluster [ build | destroy | create | update_hosts | boot | configure ] ...
+
      create base
            create a base image
 
-     create cluster CLUSTERNAME
+     create cluster [ CLUSTERNAME ]
            create a full cluster
 
      create node CLUSTERNAME IP_OFFSET
@@ -69,14 +68,11 @@ EOF
      mount DISK
            mount a qemu disk on mnt/
 
-     unmount
+     unmount | umount
            unmount a qemu disk from mnt/
 
      bootbase
            boot the base image
-
-     testproxy
-           test your proxy setup
 EOF
     exit 1
 }
@@ -84,8 +80,45 @@ EOF
 ###############################
 
 die () {
-    fill_text 0 "ERROR: $*" >&2
-    exit 1
+    if [ "$no_sanity" = 1 ] ; then
+       fill_text 0 "WARNING: $*" >&2
+    else
+       fill_text 0 "ERROR: $*" >&2
+       exit 1
+    fi
+}
+
+announce ()
+{
+    echo "######################################################################"
+    printf "# %-66s #\n" "$*"
+    echo "######################################################################"
+    echo ""
+}
+
+waitfor ()
+{
+    local file="$1"
+    local msg="$2"
+    local timeout="$3"
+
+    local tmpfile=$(mktemp)
+
+    cat <<EOF >"$tmpfile"
+spawn tail -n 10000 -f $file
+expect -timeout $timeout -re "$msg"
+EOF
+
+    export LANG=C
+    expect "$tmpfile"
+    rm -f "$tmpfile"
+
+    if ! grep -E "$msg" "$file" > /dev/null; then
+       echo "Failed to find \"$msg\" in \"$file\""
+       return 1
+    fi
+
+    return 0
 }
 
 ###############################
@@ -154,15 +187,32 @@ register_hook ()
 run_hooks ()
 {
     local hook_var="$1"
+    shift
 
     local i
     for i in ${!hook_var} ; do
-       $i
+       $i "$@"
     done
 }
 
+# Use with care, since this may clear some autocluster defaults.!
+clear_hooks ()
+{
+    local hook_var="$1"
+
+    eval "$hook_var=\"\""
+}
+
 ##############################
 
+# These hooks are intended to customise the value of $DISK.  They have
+# access to 1 argument ("base", "system", "shared") and the variables
+# $VIRTBASE, $CLUSTER, $BASENAME (for "base"), $NAME (for "system"),
+# $SHARED_DISK_NUM (for "shared").  A hook must be deterministic and
+# should not be stateful, since they can be called multiple times for
+# the same disk.
+hack_disk_hooks=""
+
 # common node creation stuff
 create_node_COMMON ()
 {
@@ -171,26 +221,55 @@ create_node_COMMON ()
     local type="$3"
     local template_file="${4:-$NODE_TEMPLATE}"
 
-    if [ "$SYSTEM_DISK_FORMAT" = "raw" -a "$BASE_FORMAT" != "raw" ] ; then
-       die "Error: if SYSTEM_DISK_FORMAT is \"raw\" then BASE_FORMAT must also be \"raw\"."
+    if [ "$SYSTEM_DISK_FORMAT" != "qcow2" -a "$BASE_FORMAT" = "qcow2" ] ; then
+       die "Error: if BASE_FORMAT is \"qcow2\" then SYSTEM_DISK_FORMAT must also be \"qcow2\"."
+    fi
+
+    local IPNUM=$(($FIRSTIP + $ip_offset))
+    make_network_map
+
+    # Determine base image name.  We use $DISK temporarily to allow
+    # the path to be hacked.
+    local DISK="${VIRTBASE}/${BASENAME}.${BASE_FORMAT}"
+    if [ "$BASE_PER_NODE_TYPE" = "yes" ] ; then
+       DISK="${VIRTBASE}/${BASENAME}-${type}.${BASE_FORMAT}"
     fi
+    run_hooks hack_disk_hooks "base"
+    local base_disk="$DISK"
 
-    IPNUM=$(($FIRSTIP + $ip_offset))
+    # Determine the system disk image name.
     DISK="${VIRTBASE}/${CLUSTER}/${NAME}.${SYSTEM_DISK_FORMAT}"
-    local base_disk="${VIRTBASE}/${BASENAME}.${BASE_FORMAT}"
+    run_hooks hack_disk_hooks "system"
 
-    mkdir -p $VIRTBASE/$CLUSTER tmp
+    local di="$DISK"
+    if [ "$DISK_FOLLOW_SYMLINKS" = "yes" -a -L "$DISK" ] ; then
+       di=$(readlink "$DISK")
+    fi
+    rm -f "$di"
+    local di_dirname="${di%/*}"
+    mkdir -p "$di_dirname"
 
-    rm -f "$DISK"
     case "$SYSTEM_DISK_FORMAT" in
        qcow2)
            echo "Creating the disk..."
-           qemu-img create -b "$base_disk" -f qcow2 "$DISK"
+           qemu-img create -b "$base_disk" -f qcow2 "$di"
            create_node_configure_image "$DISK" "$type"
            ;;
        raw)
            echo "Creating the disk..."
-           cp -v --sparse=always "$base_disk" "$DISK"
+           cp -v --sparse=always "$base_disk" "$di"
+           create_node_configure_image "$DISK" "$type"
+           ;;
+       reflink)
+           echo "Creating the disk..."
+           cp -v --reflink=always "$base_disk" "$di"
+           create_node_configure_image "$DISK" "$type"
+           ;;
+       mmclone)
+           echo "Creating the disk (using mmclone)..."
+           local base_snap="${base_disk}.snap"
+           [ -f "$base_snap" ] || mmclone snap "$base_disk" "$base_snap"
+           mmclone copy "$base_snap" "$di"
            create_node_configure_image "$DISK" "$type"
            ;;
        none)
@@ -200,9 +279,11 @@ create_node_COMMON ()
            die "Error: unknown SYSTEM_DISK_FORMAT=\"${SYSTEM_DISK_FORMAT}\"."
     esac
 
-    set_macaddrs $CLUSTER $ip_offset
-    UUID=`uuidgen`
+    # Pull the UUID for this node out of the map.
+    UUID=$(awk "\$1 == $ip_offset {print \$2}" $uuid_map)
     
+    mkdir -p tmp
+
     echo "Creating $NAME.xml"
     substitute_vars $template_file tmp/$NAME.xml
     
@@ -216,10 +297,9 @@ create_node_configure_image ()
     local disk="$1"
     local type="$2"
 
-    mount_disk "$disk"
+    diskimage mount "$disk"
     setup_base "$type"
-    setup_network
-    unmount_disk
+    diskimage unmount
 }
 
 # Provides an easy way of removing nodes from $NODE.
@@ -227,6 +307,48 @@ create_node_null () {
     :
 }
 
+hack_network_map_hooks=""
+
+# Uses: CLUSTER, NAME, NETWORKS, FIRSTIP, ip_offset
+make_network_map ()
+{
+    network_map="tmp/network_map.$NAME"
+
+    if [ -n "$CLUSTER" ] ; then
+       local md5=$(echo "$CLUSTER" | md5sum)
+       local nh=$(printf "%02x" $ip_offset)
+       local mac_prefix="02:${md5:0:2}:${md5:2:2}:00:${nh}:"
+    else
+       local mac_prefix="02:42:42:00:00:"
+    fi
+
+    local n
+    local count=1
+    for n in $NETWORKS ; do
+       local ch=$(printf "%02x" $count)
+       local mac="${mac_prefix}${ch}"
+
+       set -- ${n//,/ }
+       local ip_bits="$1" ; shift
+       local dev="$1" ; shift
+       local opts="$*"
+
+       local net="${ip_bits%/*}"
+       local netname="acnet_${net//./_}"
+
+       local ip="${net%.*}.${IPNUM}"
+       local mask="255.255.255.0"
+
+       # This can be used to override the variables in the echo
+       # statement below.  The hook can use any other variables
+       # available in this function.
+       run_hooks hack_network_map_hooks
+
+       echo "${netname} ${dev} ${ip} ${mask} ${mac} ${opts}"
+       count=$(($count + 1))
+    done >"$network_map"
+}
+
 ##############################
 
 hack_nodes_functions=
@@ -307,6 +429,8 @@ sanity_check_cluster_name ()
 Some cluster filesystems have problems with other characters."
 }
 
+hosts_file=
+
 common_nodelist_hacking ()
 {
     # Rework the NODES list
@@ -320,7 +444,7 @@ common_nodelist_hacking ()
 
        local sname=""
        local hosts_line
-       local ip_addr="$IPBASE.0.$(($FIRSTIP + $ip_offset))"
+       local ip_addr="${NETWORK_PRIVATE_PREFIX}.$(($FIRSTIP + $ip_offset))"
        
        if [ "$ctdb_node" = 1 ] ; then
            num_ctdb_nodes=$(($num_ctdb_nodes + 1))
@@ -359,21 +483,32 @@ common_nodelist_hacking ()
     ctdb_nodes_line ()
     {
        [ "$ctdb_node" = 1 ] || return 0
-       echo "$IPBASE.0.$(($FIRSTIP + $ip_offset))"
+       echo "${NETWORK_PRIVATE_PREFIX}.$(($FIRSTIP + $ip_offset))"
        num_nodes=$(($num_nodes + 1))
     }
     nodes_file="tmp/nodes.$CLUSTER"
     local num_nodes=0
     hack_all_nodes_with ctdb_nodes_line >$nodes_file
     : "${NUMNODES:=${num_nodes}}"  # Set $NUMNODES if necessary
+
+    # Build UUID map
+    uuid_map="tmp/uuid_map.$CLUSTER"
+    uuid_map_line ()
+    {
+       echo "${ip_offset} $(uuidgen) ${node_type}"
+    }
+    hack_all_nodes_with uuid_map_line >$uuid_map
 }
 
 create_cluster_hooks=
 cluster_created_hooks=
 
-create_cluster ()
+cluster_create ()
 {
-    CLUSTER="$1"
+    # Use $1.  If not set then use value from configuration file.
+    CLUSTER="${1:-${CLUSTER}}"
+    announce "cluster create \"${CLUSTER}\""
+    [ -n "$CLUSTER" ] || die "\$CLUSTER not set"
 
     sanity_check_cluster_name
 
@@ -387,10 +522,112 @@ create_cluster ()
     for_each_node call_func create_node
 
     echo "Cluster $CLUSTER created"
+    echo ""
+
+    run_hooks cluster_created_hooks
+}
+
+cluster_created_hosts_message ()
+{
     echo "You may want to add this to your /etc/hosts file:"
     cat $hosts_file
+}
 
-    run_hooks cluster_created_hooks
+register_hook cluster_created_hooks cluster_created_hosts_message
+
+cluster_destroy ()
+{
+    announce "cluster destroy \"${CLUSTER}\""
+    [ -n "$CLUSTER" ] || die "\$CLUSTER not set"
+
+    vircmd destroy "$CLUSTER" || true
+}
+
+cluster_update_hosts ()
+{
+    announce "cluster update_hosts \"${CLUSTER}\""
+    [ -n "$CLUSTER" ] || die "\$CLUSTER not set"
+
+    [ -n "$hosts_file" ] || hosts_file="tmp/hosts.${CLUSTER}"
+    [ -r "$hosts_file" ] || die "Missing hosts file \"${hosts_file}\""
+
+    local pat="# autocluster ${CLUSTER}\$|[[:space:]]${CLUSTER}(n|base)[[:digit:]]+"
+
+    local t="/etc/hosts.${CLUSTER}"
+    grep -E "$pat" /etc/hosts >"$t" || true
+    if diff -B "$t" "$hosts_file" >/dev/null ; then
+       rm "$t"
+       return
+    fi
+
+    local old=/etc/hosts.old.autocluster
+    cp /etc/hosts "$old"
+    local new=/etc/hosts.new
+    grep -Ev "$pat" "$old" |
+    cat -s - "$hosts_file" >"$new"
+
+    mv "$new" /etc/hosts
+
+    echo "Made these changes to /etc/hosts:"
+    diff -u "$old" /etc/hosts || true
+}
+
+cluster_boot ()
+{
+    [ -n "$CLUSTER_PATTERN" ] || CLUSTER_PATTERN="$CLUSTER"
+    announce "cluster boot \"${CLUSTER_PATTERN}\""
+    [ -n "$CLUSTER_PATTERN" ] || die "\$CLUSTER_PATTERN not set"
+
+    vircmd start "$CLUSTER_PATTERN"
+
+    local nodes=$(vircmd dominfo "$CLUSTER_PATTERN" 2>/dev/null | \
+       sed -n -e 's/Name: *//p')
+
+    # Wait for each node
+    local i
+    for i in $nodes ; do
+       waitfor "${KVMLOG}/serial.$i" "login:" 300 || {
+           vircmd destroy "$CLUSTER_PATTERN"
+           die "Failed to create cluster"
+       }
+    done
+
+    # Move past the last line of log output
+    echo ""
+}
+
+cluster_configure ()
+{
+    announce "cluster configure \"${CLUSTER}\""
+    [ -n "$CLUSTER" ] || die "\$CLUSTER not set"
+
+    local n1="${CLUSTER}n1"
+    local ssh="ssh -o StrictHostKeyChecking=no"
+
+    case "$CLUSTER_TYPE" in
+       "build")
+           $ssh "$n1" ./scripts/install_packages.sh clusterfs build
+           $ssh "$n1" ./scripts/setup_cluster.sh build
+           ;;
+
+       "ad")
+           $ssh "$n1" ./scripts/install_packages.sh ad_server
+           $ssh "$n1" ./scripts/configure_cluster.sh ad_server
+           ;;
+
+       "samba")
+           [ -n "$CLUSTER_PATTERN" ] || CLUSTER_PATTERN="$CLUSTER"
+
+           local nodes=$(vircmd dominfo "$CLUSTER_PATTERN" 2>/dev/null | \
+               sed -n -e 's/Name: *//p')
+
+           for i in $nodes ; do
+               $ssh "$i" ./scripts/install_packages.sh clusterfs nas
+           done
+
+           $ssh "$n1" ./scripts/setup_cluster.sh clusterfs nas
+           ;;
+    esac
 }
 
 create_one_node ()
@@ -410,6 +647,7 @@ create_one_node ()
        call_func create_node "$@"
        
        echo "Requested node created"
+       echo ""
        echo "You may want to update your /etc/hosts file:"
        cat $hosts_file
        
@@ -430,42 +668,61 @@ test_proxy() {
 
 kickstart_floppy_create_hooks=
 
-# create base image
-create_base() {
+guess_install_network ()
+{
+    # Figure out IP address to use during base install.  Default to
+    # the IP address of the 1st (private) network. If a gateway is
+    # specified then use the IP address associated with it.
+    INSTALL_IP=""
+    INSTALL_GW=""
+    local netname dev ip mask mac opts
+    while read netname dev ip mask mac opts; do
+       local o
+       for o in $opts ; do
+           case "$o" in
+               gw\=*)
+                   INSTALL_GW="${o#gw=}"
+                   INSTALL_IP="${ip}${FIRSTIP}"
+           esac
+       done
+       [ -n "$INSTALL_IP" ] || INSTALL_IP="$ip"
+    done <"$network_map"
+}
 
-    NAME="$BASENAME"
-    DISK="${VIRTBASE}/${NAME}.${BASE_FORMAT}"
+# create base image
+base_create()
+{
+    local NAME="$BASENAME"
+    local DISK="${VIRTBASE}/${NAME}.${BASE_FORMAT}"
+    run_hooks hack_disk_hooks "base"
 
     mkdir -p $KVMLOG
 
     echo "Testing WEBPROXY $WEBPROXY"
     test_proxy
 
+    local di="$DISK"
+    if [ "$DISK_FOLLOW_SYMLINKS" = "yes" -a -L "$DISK" ] ; then
+       di=$(readlink "$DISK")
+    fi
+    rm -f "$di"
+    local di_dirname="${di%/*}"
+    mkdir -p "$di_dirname"
+
     echo "Creating the disk"
-    qemu-img create -f $BASE_FORMAT "$DISK" $DISKSIZE
+    qemu-img create -f $BASE_FORMAT "$di" $DISKSIZE
 
     rm -rf tmp
     mkdir -p mnt tmp tmp/ISO
 
     setup_timezone
 
-    echo "Creating kickstart file from template"
-    substitute_vars "$KICKSTART" "tmp/ks.cfg"
-
-    if [ $INSTALLKEY = "--skip" ]; then
-       cat <<EOF
---------------------------------------------------------------------------------------
-WARNING: You have not entered an install key. Some RHEL packages will not be installed.
-
-Please enter a valid RHEL install key in your config file like this:
+    make_network_map
 
-  INSTALLKEY="1234-5678-0123-4567"
+    guess_install_network
 
-The install will continue without an install key in 5 seconds
---------------------------------------------------------------------------------------
-EOF
-       sleep 5
-    fi
+    echo "Creating kickstart file from template"
+    substitute_vars "$KICKSTART" "tmp/ks.cfg"
 
     # $ISO gets $ISO_DIR prepended if it doesn't start with a leading '/'.
     case "$ISO" in
@@ -475,7 +732,7 @@ EOF
     
     echo "Creating kickstart floppy"
     dd if=/dev/zero of=tmp/floppy.img bs=1024 count=1440
-    mkdosfs tmp/floppy.img
+    mkdosfs -n KICKSTART tmp/floppy.img
     mount -o loop -t msdos tmp/floppy.img mnt
     cp tmp/ks.cfg mnt
     mount -o loop,ro $ISO tmp/ISO
@@ -503,9 +760,13 @@ EOF
     sleep 2
     
     # wait for the install to finish
-    if ! waitfor $KVMLOG/serial.$NAME "you may safely reboot your system" $CREATE_BASE_TIMEOUT ; then
+    if ! waitfor $KVMLOG/serial.$NAME "$KS_DONE_MESSAGE" $CREATE_BASE_TIMEOUT ; then
        $VIRSH destroy $NAME
-       die "Failed to create base image $DISK"
+       die "Failed to create base image ${DISK} after waiting for ${CREATE_BASE_TIMEOUT} seconds.
+You may need to increase the value of CREATE_BASE_TIMEOUT.
+Alternatively, the install might have completed but KS_DONE_MESSAGE
+(currently \"${KS_DONE_MESSAGE}\")
+may not have matched anything at the end of the kickstart output."
     fi
     
     $VIRSH destroy $NAME
@@ -516,6 +777,7 @@ EOF
 Install finished, base image $DISK created
 
 You may wish to run
+   chcon -t virt_content_t $DISK
    chattr +i $DISK
 To ensure that this image does not change
 
@@ -526,21 +788,22 @@ EOF
 
 ###############################
 # boot the base disk
-boot_base() {
-    CLUSTER="$1"
+base_boot() {
+    rm -rf tmp
+    mkdir -p tmp
 
     NAME="$BASENAME"
     DISK="${VIRTBASE}/${NAME}.${BASE_FORMAT}"
 
-    rm -rf tmp
-    mkdir -p tmp
-
     IPNUM=$FIRSTIP
+
+    make_network_map
+
     CLUSTER="base"
 
-    mount_disk $DISK
+    diskimage mount $DISK
     setup_base
-    unmount_disk
+    diskimage unmount
 
     UUID=`uuidgen`
     
@@ -553,121 +816,13 @@ boot_base() {
 
 ######################################################################
 
-# various functions...
+# Updating a disk image...
 
-# Set some MAC address variables based on a hash of the cluster name
-# plus the node number and each adapter number.
-set_macaddrs () {
-    local cname="$1"
-    local ip_offset="$2"
-
-    local md5=$(echo $cname | md5sum)
-    local nh=$(printf "%02x" $ip_offset)
-    local mac_prefix="02:${md5:0:2}:${md5:2:2}:00:${nh}:"
-
-    MAC1="${mac_prefix}01"
-    MAC2="${mac_prefix}02"
-    MAC3="${mac_prefix}03"
-    MAC4="${mac_prefix}04"
-    MAC5="${mac_prefix}05"
-    MAC6="${mac_prefix}06"
-}
-
-# mount a qemu image via nbd
-connect_nbd() {    
-    echo "Connecting nbd to $1"
-    mkdir -p mnt
-    modprobe nbd
-    killall -9 -q $QEMU_NBD || true
-    $QEMU_NBD -p 1300 $1 &
-    sleep 1
-    [ -r $NBD_DEVICE ] || {
-       mknod $NBD_DEVICE b 43 0
-    }
-    umount mnt 2> /dev/null || true
-    nbd-client -d $NBD_DEVICE > /dev/null 2>&1 || true
-    killall -9 -q nbd-client || true
-    nbd-client localhost 1300 $NBD_DEVICE > /dev/null 2>&1 || true &
-    sleep 1
-}
-
-# disconnect nbd
-disconnect_nbd() {
-    echo "Disconnecting nbd"
-    sync; sync
-    nbd-client -d $NBD_DEVICE > /dev/null 2>&1 || true
-    killall -9 -q nbd-client || true
-    killall -q $QEMU_NBD || true
-}
-
-setup_image ()
-{
-    local disk="$1"
-
-    case "$SYSTEM_DISK_FORMAT" in
-       qcow2)
-           connect_nbd "$disk"
-           device=$NBD_DEVICE
-           extra_mount_options=""
-           ;;
-       raw)
-           device="$disk"
-           extra_mount_options=",loop"
-           ;;
-       *)
-           die "Error: unknown SYSTEM_DISK_FORMAT=${SYSTEM_DISK_FORMAT}."
-    esac
-}
-
-cleanup_image ()
-{
-    case "$SYSTEM_DISK_FORMAT" in
-       qcow2)
-           disconnect_nbd
-           ;;
-       raw)
-           :
-           ;;
-       *)
-           die "Error: unknown SYSTEM_DISK_FORMAT=${SYSTEM_DISK_FORMAT}."
-    esac
-}
-
-# mount a qemu image via nbd
-mount_disk()
+diskimage ()
 {
-    local disk="$1"
-
-    local device extra_mount_options
-
-    setup_image "$disk"
-
-    echo "Mounting disk $disk"
-    local mount_ok=0
-    local i
-    for i in $(seq 1 5); do
-       mount -o offset=32256${extra_mount_options} $device mnt && {
-           mount_ok=1
-           break
-       }
-       umount mnt 2>/dev/null || true
-       sleep 1
-    done
-    [ $mount_ok = 1 ] || die "Failed to mount $disk"
-
-    [ -d mnt/root ] || {
-       echo "Mounted directory does not look like a root filesystem"
-       ls -latr mnt
-       exit 1
-    }
-}
-
-# unmount a qemu image
-unmount_disk() {
-    echo "Unmounting disk"
-    sync; sync;
-    umount mnt || umount mnt || true
-    cleanup_image
+    local func="$1"
+    shift
+    call_func diskimage_"$func" "$SYSTEM_DISK_ACCESS_METHOD" "$@"
 }
 
 # setup the files from $BASE_TEMPLATES/, substituting any variables
@@ -680,73 +835,145 @@ copy_base_dir_substitute_templates ()
     [ -d "$d" ] || return 0
 
     local f
-    for f in $(cd "$d" && find . \! -name '*~') ; do
+    for f in $(cd "$d" && find . \! -name '*~' \( -type d -name .svn -prune -o -print \) ) ; do
+       f="${f#./}" # remove leading "./" for clarity
        if [ -d "$d/$f" ]; then
-           mkdir -p mnt/"$f"
-       else 
-           substitute_vars "$d/$f" "mnt/$f"
+           # Don't chmod existing directory
+           if diskimage is_directory "/$f" ; then
+               continue
+           fi
+           diskimage mkdir_p "/$f"
+       else
+           echo " Install: $f"
+           diskimage substitute_vars "$d/$f" "/$f"
        fi
-       chmod --reference="$d/$f" "mnt/$f"
+       diskimage chmod_reference "$d/$f" "/$f"
     done
 }
 
 setup_base_hooks=
 
-setup_base()
+setup_base_ssh_keys ()
 {
-    local type="$1"
-
-    umask 022
-    echo "Copy base files"
-    copy_base_dir_substitute_templates "all"
-    if [ -n "$type" ] ; then
-       copy_base_dir_substitute_templates "$type"
-    fi
-
     # this is needed as git doesn't store file permissions other
     # than execute
-    chmod 600 mnt/etc/ssh/*key mnt/root/.ssh/*
-    chmod 700 mnt/etc/ssh mnt/root/.ssh mnt/root
+    # Note that we protect the wildcards from the local shell.
+    diskimage chmod 600 "/etc/ssh/*key" "/root/.ssh/*"
+    diskimage chmod 700 "/etc/ssh" "/root/.ssh" "/root"
     if [ -r "$HOME/.ssh/id_rsa.pub" ]; then
        echo "Adding $HOME/.ssh/id_rsa.pub to ssh authorized_keys"
-       cat "$HOME/.ssh/id_rsa.pub" >> mnt/root/.ssh/authorized_keys
+       diskimage append_text_file "$HOME/.ssh/id_rsa.pub" "/root/.ssh/authorized_keys"
     fi
     if [ -r "$HOME/.ssh/id_dsa.pub" ]; then
        echo "Adding $HOME/.ssh/id_dsa.pub to ssh authorized_keys"
-       cat "$HOME/.ssh/id_dsa.pub" >> mnt/root/.ssh/authorized_keys
+       diskimage append_text_file "$HOME/.ssh/id_dsa.pub" "/root/.ssh/authorized_keys"
     fi
+}
+
+register_hook setup_base_hooks setup_base_ssh_keys
+
+setup_base_grub_conf ()
+{
     echo "Adjusting grub.conf"
     local o="$EXTRA_KERNEL_OPTIONS" # For readability.
-    sed -e "s/console=ttyS0,19200/console=ttyS0,115200/"  \
-       -e "s/ nodmraid//" -e "s/ nompath//"  \
-       -e "s/quiet/noapic divider=10${o:+ }${o}/g" mnt/boot/grub/grub.conf -i.org
+    local grub_configs="/boot/grub/grub.conf"
+    if ! diskimage is_file "$grub_configs" ; then
+       grub_configs="/etc/default/grub /boot/grub2/grub.cfg"
+    fi
+    local c
+    for c in $grub_configs ; do
+       diskimage sed "$c" \
+           -e "s/console=ttyS0,19200/console=ttyS0,115200/"  \
+           -e "s/ console=tty1//" -e "s/ rhgb/ norhgb/"  \
+           -e "s/ nodmraid//" -e "s/ nompath//"  \
+           -e "s/quiet/noapic divider=10${o:+ }${o}/g"
+    done
+}
+
+register_hook setup_base_hooks setup_base_grub_conf
+
+setup_base()
+{
+    local type="$1"
+
+    umask 022
+    echo "Copy base files"
+    copy_base_dir_substitute_templates "all"
+    if [ -n "$type" ] ; then
+       copy_base_dir_substitute_templates "$type"
+    fi
+
     run_hooks setup_base_hooks
 }
 
 # setup various networking components
-setup_network() {
-    echo "Setting up networks"
+setup_network()
+{
+    # This avoids doing anything when we're called from boot_base().
+    if [ -z "$hosts_file" ] ; then
+       echo "Skipping network-related setup"
+       return
+    fi
 
-    cat $hosts_file >>mnt/etc/hosts
+    echo "Setting up networks"
+    diskimage append_text_file "$hosts_file" "/etc/hosts"
 
     echo "Setting up /etc/ctdb/nodes"
-    mkdir -p mnt/etc/ctdb
-    cp $nodes_file mnt/etc/ctdb/nodes
+    diskimage mkdir_p "/etc/ctdb"
+    diskimage put "$nodes_file" "/etc/ctdb/nodes"
 
     [ "$WEBPROXY" = "" ] || {
-       echo "export http_proxy=$WEBPROXY" >> mnt/etc/bashrc
+       diskimage append_text "export http_proxy=$WEBPROXY" "/etc/bashrc"
     }
 
     if [ -n "$NFSSHARE" -a -n "$NFS_MOUNTPOINT" ] ; then
        echo "Enabling nfs mount of $NFSSHARE"
-       mkdir -p "mnt$NFS_MOUNTPOINT"
-       echo "$NFSSHARE $NFS_MOUNTPOINT nfs intr" >> mnt/etc/fstab
+       diskimage mkdir_p "$NFS_MOUNTPOINT"
+       diskimage append_text "$NFSSHARE $NFS_MOUNTPOINT nfs nfsvers=3,intr 0 0" "/etc/fstab"
     fi
 
-    mkdir -p mnt/etc/yum.repos.d
-    echo '@@@YUM_TEMPLATE@@@' | substitute_vars - > mnt/etc/yum.repos.d/autocluster.repo
+    diskimage mkdir_p "/etc/yum.repos.d"
+    echo '@@@YUM_TEMPLATE@@@' | diskimage substitute_vars - "/etc/yum.repos.d/autocluster.repo"
+
+    diskimage rm_rf "/etc/udev/rules.d/70-persistent-net.rules"
+
+    echo "Setting up network interfaces: "
+    local netname dev ip mask mac opts
+    while read netname dev ip mask mac opts; do
+       echo "  $dev"
+
+       local o gw
+       gw=""
+       for o in $opts ; do
+           case "$o" in
+               gw\=*)
+                   gw="${o#gw=}"
+           esac
+       done
+
+       cat <<EOF | \
+           diskimage put - "/etc/sysconfig/network-scripts/ifcfg-${dev}"
+DEVICE=$dev
+ONBOOT=yes
+TYPE=Ethernet
+IPADDR=$ip
+NETMASK=$mask
+HWADDR=$mac
+${gw:+GATEWAY=}${gw}
+EOF
+
+       # This goes to 70-persistent-net.rules
+       cat <<EOF
+# Generated by autocluster
+SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="${mac}", ATTR{type}=="1", KERNEL=="eth*", NAME="${dev}"
+
+EOF
+    done <"$network_map" |
+    diskimage put - "/etc/udev/rules.d/70-persistent-net.rules"
 }
 
+register_hook setup_base_hooks setup_network
+
 setup_timezone() {
     [ -z "$TIMEZONE" ] && {
        [ -r /etc/timezone ] && {
@@ -773,32 +1000,44 @@ substitute_vars() {(
        infile="${1:-/dev/null}" # if empty then default to /dev/null
        outfile="$2" # optional
 
-       instring=$(cat $infile)
+       tmp_out=$(mktemp)
+       cat "$infile" >"$tmp_out"
 
        # Handle any indirects by looping until nothing changes.
        # However, only handle 10 levels of recursion.
        count=0
        while : ; do
-           outstring=$(_substitute_vars "$instring" "@@@")
-           [ $? -eq 0 ] || die "Failed to expand template $infile"
+           if ! _substitute_vars "$tmp_out" "@@@" ; then
+               rm -f "$tmp_out"
+               die "Failed to expand template $infile"
+           fi
 
-           [ "$instring" = "$outstring" ] && break
+           # No old version of file means no changes made.
+           if [ ! -f "${tmp_out}.old" ] ; then
+               break
+           fi
+
+           rm -f "${tmp_out}.old"
 
            count=$(($count + 1))
-           [ $count -lt 10 ] || \
+           if [ $count -ge 10 ] ; then
+               rm -f "$tmp_out"
                die "Recursion too deep in $infile - only 10 levels allowed!"
-
-           instring="$outstring"
+           fi
        done
 
        # Now regular variables.
-       outstring=$(_substitute_vars "$instring" "@@")
-       [ $? -eq 0 ] || die "Failed to expand template $infile"
+       if ! _substitute_vars "$tmp_out" "@@" ; then
+           rm -f "$tmp_out"
+           die "Failed to expand template $infile"
+       fi
+       rm -f "${tmp_out}.old"
 
        if [ -n "$outfile" ] ; then
-           echo "$outstring" > "$outfile"
+           mv "$tmp_out" "$outfile"
        else
-           echo "$outstring"
+           cat "$tmp_out"
+           rm -f "$tmp_out"
        fi
 )}
 
@@ -808,14 +1047,15 @@ substitute_vars() {(
 # @@@ supports leading '|' in variable value, which means to excute a
 # command.
 _substitute_vars() {(
-       instring="$1"
+       tmp_out="$1"
        delimiter="${2:-@@}"
 
-       # get the list of variables used in the template
-       VARS=`echo "$instring" |
-             tr -cs "A-Z0-9_$delimiter" '\012' | 
-              sort -u |
-             sed -n -e "s#^${delimiter}\(.*\)${delimiter}\\$#\1#p"`
+       # Get the list of variables used in the template.  The grep
+       # gets rid of any blank lines and lines with extraneous '@'s
+       # next to template substitutions.
+       VARS=$(sed -n -e "s#[^@]*${delimiter}\([A-Z0-9_][A-Z0-9_]*\)${delimiter}[^@]*#\1\n#gp" "$tmp_out" |
+           grep '^[A-Z0-9_][A-Z0-9_]*$' |
+           sort -u)
 
        tmp=$(mktemp)
        for v in $VARS; do
@@ -848,14 +1088,21 @@ _substitute_vars() {(
            fi
 
            # escape some pesky chars
-           s=${s//
-/\\n}
+           # This first one can be too slow if done using a bash
+           # variable pattern subsitution.
+           s=$(echo -n "$s" | tr '\n' '\001' | sed -e 's/\o001/\\n/g')
            s=${s//#/\\#}
            s=${s//&/\\&}
            echo "s#${delimiter}${v}${delimiter}#${s}#g"
        done > $tmp
 
-       echo "$instring" | sed -f $tmp
+       # Get the in-place sed to make a backup of the old file.
+       # Remove the backup if it is the same as the resulting file -
+       # this acts as a flag to the caller that no changes were made.
+       sed -i.old -f $tmp "$tmp_out"
+       if cmp -s "${tmp_out}.old" "$tmp_out" ; then
+           rm -f "${tmp_out}.old"
+       fi
 
        rm -f $tmp
 )}
@@ -1040,80 +1287,29 @@ usage_config_options (){
     usage_smart_display load_config
 }
 
-list_releases () {
-    local releases=$(cd $installdir/releases && echo *.release)
-    releases="${releases//.release}"
-    releases="${releases// /\", \"}"
-    echo "\"$releases\""
-}
-
-with_release () {
-    local release="$1"
-    shift # subsequent args are passed to release file
-
-    # This simply loads an extra config file from $installdir/releases
-    f="${installdir}/releases/${release}.release"
-    if [ -r "$f" ] ; then
-       . "$f"
-    else
-       f="${installdir}/releases/${release%%-*}.release"
-       if [ -r "$f" ] ; then
-           . "$f" "${release#*-}"
-       else
-           echo "Unknown release \"${release}\" specified to --with-release"
-           printf "%-25s" "Supported releases are: "
-           fill_text 25 "$(list_releases)"
-           exit 1
-       fi
-    fi
-
+actions_init ()
+{
+    actions=""
 }
 
-has_public_addresses_DEFAULT ()
+actions_add ()
 {
-    false
+    actions="${actions}${actions:+ }$*"
 }
 
-# Build public address configuration.
-# * 1st public IP:  unless specified, last octet is $FIRSTIP + $PUBLIC_IP_OFFSET
-# * Excluded nodes: unless specified via comma-separated list of IP offsets,
-#                   nodes are excluded via their node types
-# * Number of public addresses per interface is either specified or $NUMNODES.
-make_public_addresses () {
-    local firstip="${1:-$(($FIRSTIP + $PUBLIC_IP_OFFSET))}"
-    local excluded_nodes="$2" 
-    local num_addrs="${3:-${NUMNODES}}"
-
-    # For delimiting matches.
-    excluded_nodes="${excluded_nodes:+,}${excluded_nodes}${excluded_nodes:+,}"
-    # Avoid spaces
-    excluded_nodes="${excluded_nodes// /}"
-
-    make_public_addresses_for_node ()
-    {
-       [ "$ctdb_node" = 1 ] || return 0
+actions_run ()
+{
+    [ -n "$actions" ] || usage
 
-       echo "[/etc/ctdb/public_addresses:${name}.${DOMAIN}]"
+    local a
+    for a in $actions ; do
+       $a
+    done
+}
 
-       if [ -n "$excluded_nodes" -a \
-           "${excluded_nodes/,${ip_offset},}" = "$excluded_nodes" ] ||
-           ([ -z "$excluded_nodes" ] &&
-               call_func has_public_addresses "$node_type") ; then
+######################################################################
 
-           local e i
-           for e in "1" "2" ; do
-               for i in $(seq $firstip $(($firstip + $num_addrs - 1))) ; do
-                   if [ $i -gt 254 ] ; then
-                       die "make_public_addresses: octet > 254 - consider setting PUBLIC_IP_OFFSET"
-                   fi
-                   printf "\t${IPBASE}.${e}.${i}/24 eth${e}\n"
-               done
-           done            
-       fi
-       echo 
-    }
-    hack_all_nodes_with make_public_addresses_for_node
-}
+post_config_hooks=
 
 ######################################################################
 
@@ -1122,7 +1318,7 @@ load_config
 ############################
 # parse command line options
 long_opts=$(getopt_config_options)
-getopt_output=$(getopt -n autocluster -o "c:e:xh" -l help,dump,with-release: -l "$long_opts" -- "$@")
+getopt_output=$(getopt -n autocluster -o "c:e:E:xh" -l help,dump -l "$long_opts" -- "$@")
 [ $? != 0 ] && usage
 
 use_default_config=true
@@ -1134,8 +1330,8 @@ while true ; do
     case "$1" in
        -c) shift 2 ; use_default_config=false ;;
        -e) shift 2 ;;
+       -E) shift 2 ;;
        --) shift ; break ;;
-       --with-release) shift 2 ;; # Don't set use_default_config=false!!!
        --dump|-x) shift ;;
        -h|--help) usage ;; # Usage should be shown here for real defaults.
        --*) shift 2 ;; # Assume other long opts are valid and take an arg.
@@ -1150,15 +1346,24 @@ eval set -- "$getopt_output"
 
 while true ; do
     case "$1" in
-       # force at least ./local_file to avoid accidental file from $PATH
-       -c) . "$(dirname $2)/$(basename $2)" ; shift 2 ;;
-       -e) eval "$2" ; exit ;;
-       --with-release)
-           with_release "$2"
+       -c)
+           b=$(basename $2)
+           # force at least ./local_file to avoid accidental file
+           # from $PATH
+           . "$(dirname $2)/${b}"
+           # If $CLUSTER is unset then try to base it on the filename
+           if [ ! -n "$CLUSTER" ] ; then
+               case "$b" in
+                   *.autocluster)
+                       CLUSTER="${b%.autocluster}"
+               esac
+           fi
            shift 2
            ;;
+       -e) no_sanity=1 ; run_hooks post_config_hooks ; eval "$2" ; exit ;;
+       -E) eval "$2" ; shift 2 ;;
        -x) set -x; shift ;;
-       --dump) dump_config ;;
+       --dump) no_sanity=1 ; run_hooks post_config_hooks ; dump_config ;;
        --) shift ; break ;;
        -h|--help) usage ;; # Redundant.
        --*)
@@ -1178,6 +1383,8 @@ while true ; do
     esac
 done
 
+run_hooks post_config_hooks 
+
 # catch errors
 set -e
 set -E
@@ -1187,28 +1394,52 @@ trap 'es=$?;
 
 # check for needed programs 
 check_command expect
-if [ "$SYSTEM_DISK_FORMAT" != "raw" ] ; then
-    check_command $QEMU_NBD
-    check_command nbd-client
-fi
 
 [ $# -lt 1 ] && usage
 
-command="$1"
+t="$1"
 shift
 
-case $command in
+case "$t" in
+    base)
+       actions_init
+       for t in "$@" ; do
+           case "$t" in
+               create|boot) actions_add "base_${t}" ;;
+               *) usage ;;
+           esac
+       done
+       actions_run
+       ;;
+
+    cluster)
+       actions_init
+       for t in "$@" ; do
+           case "$t" in
+               destroy|create|update_hosts|boot|configure)
+                   actions_add "cluster_${t}" ;;
+               build)
+                   for t in destroy create update_hosts boot configure ; do
+                       actions_add "cluster_${t}"
+                   done
+                   ;;
+               *) usage ;;
+           esac
+       done
+       actions_run
+       ;;
+
     create)
-       type=$1
+       t="$1"
        shift
-       case $type in
+       case "$t" in
            base)
                [ $# != 0 ] && usage
-               create_base
+               base_create
                ;;
            cluster)
                [ $# != 1 ] && usage
-               create_cluster "$1"
+               cluster_create "$1"
                ;;
            node)
                [ $# != 2 ] && usage
@@ -1221,17 +1452,14 @@ case $command in
        ;;
     mount)
        [ $# != 1 ] && usage
-       mount_disk "$1"
+       diskimage mount "$1"
        ;;
-    unmount)
+    unmount|umount)
        [ $# != 0 ] && usage
-       unmount_disk
+       diskimage unmount
        ;;
     bootbase)
-       boot_base;
-       ;;
-    testproxy)
-       test_proxy;
+       base_boot;
        ;;
     *)
        usage;