New configuration variable NETWORKS - IPBASE, IPNET* no longer used
[tridge/autocluster.git] / autocluster
1 #!/bin/bash
2 # main autocluster script
3 #
4 # Copyright (C) Andrew Tridgell  2008
5 # Copyright (C) Martin Schwenke  2008
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #   
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #   
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, see <http://www.gnu.org/licenses/>.
19
20 ##BEGIN-INSTALLDIR-MAGIC##
21 # There are better ways of doing this but not if you still want to be
22 # able to run straight out of a git tree.  :-)
23 if [ -f "$0" ]; then
24     autocluster="$0"
25 else
26     autocluster=$(which "$0")
27 fi
28 if [ -L "$autocluster" ] ; then
29     autocluster=$(readlink "$autocluster")
30 fi
31 installdir=$(dirname "$autocluster")
32 ##END-INSTALLDIR-MAGIC##
33
34 ####################
35 # show program usage
36 usage ()
37 {
38     cat <<EOF
39 Usage: autocluster [OPTION] ... <COMMAND>
40   options:
41      -c <file>                   specify config file (default is "config")
42      -e <expr>                   execute <expr> and exit
43      -E <expr>                   execute <expr> and continue
44      -x                          enable script debugging
45      --dump                      dump config settings and exit
46
47   configuration options:
48 EOF
49
50     usage_config_options
51
52     cat <<EOF
53
54   commands:
55      create base
56            create a base image
57
58      create cluster CLUSTERNAME
59            create a full cluster
60
61      create node CLUSTERNAME IP_OFFSET
62            (re)create a single cluster node
63
64      mount DISK
65            mount a qemu disk on mnt/
66
67      unmount | umount
68            unmount a qemu disk from mnt/
69
70      bootbase
71            boot the base image
72
73      testproxy
74            test your proxy setup
75 EOF
76     exit 1
77 }
78
79 ###############################
80
81 die () {
82     fill_text 0 "ERROR: $*" >&2
83     exit 1
84 }
85
86 ###############################
87
88 # Indirectly call a function named by ${1}_${2}
89 call_func () {
90     local func="$1" ; shift
91     local type="$1" ; shift
92
93     local f="${func}_${type}"
94     if type -t "$f" >/dev/null && ! type -P "$f" >/dev/null ; then
95         "$f" "$@"
96     else
97         f="${func}_DEFAULT"
98         if type -t "$f" >/dev/null && ! type -P "$f" >/dev/null  ; then
99             "$f" "$type" "$@"
100         else
101             die "No function defined for \"${func}\" \"${type}\""
102         fi
103     fi
104 }
105
106 # Note that this will work if you pass "call_func f" because the first
107 # element of the node tuple is the node type.  Nice...  :-)
108 for_each_node ()
109 {
110     local n
111     for n in $NODES ; do
112         "$@" $(IFS=: ; echo $n)
113     done
114 }
115
116 hack_one_node_with ()
117 {
118     local filter="$1" ; shift
119
120     local node_type="$1"
121     local ip_offset="$2"
122     local name="$3"
123     local ctdb_node="$4"
124
125     $filter
126
127     local item="${node_type}:${ip_offset}${name:+:}${name}${ctdb_node:+:}${ctdb_node}"
128     nodes="${nodes}${nodes:+ }${item}"
129 }
130
131 # This also gets used for non-filtering iteration.
132 hack_all_nodes_with ()
133 {
134     local filter="$1"
135
136     local nodes=""
137     for_each_node hack_one_node_with "$filter"
138     NODES="$nodes"
139 }
140
141 register_hook ()
142 {
143     local hook_var="$1"
144     local new_hook="$2"
145
146     eval "$hook_var=\"${!hook_var}${!hook_var:+ }${new_hook}\""
147 }
148
149 run_hooks ()
150 {
151     local hook_var="$1"
152     shift
153
154     local i
155     for i in ${!hook_var} ; do
156         $i "$@"
157     done
158 }
159
160 # Use with care, since this may clear some autocluster defaults.!
161 clear_hooks ()
162 {
163     local hook_var="$1"
164
165     eval "$hook_var=\"\""
166 }
167
168 ##############################
169
170 # These hooks are intended to customise the value of $DISK.  They have
171 # access to 1 argument ("base", "system", "shared") and the variables
172 # $VIRTBASE, $CLUSTER, $BASENAME (for "base"), $NAME (for "system"),
173 # $SHARED_DISK_NUM (for "shared").  A hook must be deterministic and
174 # should not be stateful, since they can be called multiple times for
175 # the same disk.
176 hack_disk_hooks=""
177
178 # common node creation stuff
179 create_node_COMMON ()
180 {
181     local NAME="$1"
182     local ip_offset="$2"
183     local type="$3"
184     local template_file="${4:-$NODE_TEMPLATE}"
185
186     if [ "$SYSTEM_DISK_FORMAT" != "qcow2" -a "$BASE_FORMAT" = "qcow2" ] ; then
187         die "Error: if BASE_FORMAT is \"qcow2\" then SYSTEM_DISK_FORMAT must also be \"qcow2\"."
188     fi
189
190     local IPNUM=$(($FIRSTIP + $ip_offset))
191
192     # Determine base image name.  We use $DISK temporarily to allow
193     # the path to be hacked.
194     local DISK="${VIRTBASE}/${BASENAME}.${BASE_FORMAT}"
195     if [ "$BASE_PER_NODE_TYPE" = "yes" ] ; then
196         DISK="${VIRTBASE}/${BASENAME}-${type}.${BASE_FORMAT}"
197     fi
198     run_hooks hack_disk_hooks "base"
199     local base_disk="$DISK"
200
201     # Determine the system disk image name.
202     DISK="${VIRTBASE}/${CLUSTER}/${NAME}.${SYSTEM_DISK_FORMAT}"
203     run_hooks hack_disk_hooks "system"
204
205     local di="$DISK"
206     if [ "$DISK_FOLLOW_SYMLINKS" = "yes" -a -L "$DISK" ] ; then
207         di=$(readlink "$DISK")
208     fi
209     rm -f "$di"
210     local di_dirname="${di%/*}"
211     mkdir -p "$di_dirname"
212
213     case "$SYSTEM_DISK_FORMAT" in
214         qcow2)
215             echo "Creating the disk..."
216             qemu-img create -b "$base_disk" -f qcow2 "$di"
217             create_node_configure_image "$DISK" "$type"
218             ;;
219         raw)
220             echo "Creating the disk..."
221             cp -v --sparse=always "$base_disk" "$di"
222             create_node_configure_image "$DISK" "$type"
223             ;;
224         reflink)
225             echo "Creating the disk..."
226             cp -v --reflink=always "$base_disk" "$di"
227             create_node_configure_image "$DISK" "$type"
228             ;;
229         mmclone)
230             echo "Creating the disk (using mmclone)..."
231             local base_snap="${base_disk}.snap"
232             [ -f "$base_snap" ] || mmclone snap "$base_disk" "$base_snap"
233             mmclone copy "$base_snap" "$di"
234             create_node_configure_image "$DISK" "$type"
235             ;;
236         none)
237             echo "Skipping disk image creation as requested"
238             ;;
239         *)
240             die "Error: unknown SYSTEM_DISK_FORMAT=\"${SYSTEM_DISK_FORMAT}\"."
241     esac
242
243     # Pull the UUID for this node out of the map.
244     UUID=$(awk "\$1 == $ip_offset {print \$2}" $uuid_map)
245     
246     mkdir -p tmp
247
248     echo "Creating $NAME.xml"
249     substitute_vars $template_file tmp/$NAME.xml
250     
251     # install the XML file
252     $VIRSH undefine $NAME > /dev/null 2>&1 || true
253     $VIRSH define tmp/$NAME.xml
254 }
255
256 create_node_configure_image ()
257 {
258     local disk="$1"
259     local type="$2"
260
261     diskimage mount "$disk"
262     setup_base "$type"
263     diskimage unmount
264 }
265
266 # Provides an easy way of removing nodes from $NODE.
267 create_node_null () {
268     :
269 }
270
271 ##############################
272
273 hack_nodes_functions=
274
275 expand_nodes () {
276     # Expand out any abbreviations in NODES.
277     local ns=""
278     local n
279     for n in $NODES ; do
280         local t="${n%:*}"
281         local ips="${n#*:}"
282         case "$ips" in
283             *,*)
284                 local i
285                 for i in ${ips//,/ } ; do
286                     ns="${ns}${ns:+ }${t}:${i}"
287                 done
288                 ;;
289             *-*)
290                 local i
291                 for i in $(seq ${ips/-/ }) ; do
292                     ns="${ns}${ns:+ }${t}:${i}"
293                 done
294                 ;;
295             *)
296                 ns="${ns}${ns:+ }${n}"
297         esac
298     done
299     NODES="$ns"
300
301     # Apply nodes hacks.  Some of this is about backward compatibility
302     # but the hacks also fill in the node names and whether they're
303     # part of the CTDB cluster.  The order is the order that
304     # configuration modules register their hacks.
305     run_hooks hack_nodes_functions
306
307     if [ -n "$NUMNODES" ] ; then
308         # Attempt to respect NUMNODES.  Reduce the number of CTDB
309         # nodes to NUMNODES.
310         local numnodes=$NUMNODES
311
312         hack_filter ()
313         {
314             if [ "$ctdb_node" = 1 ] ; then
315                 if [ $numnodes -gt 0 ] ; then
316                     numnodes=$(($numnodes - 1))
317                 else
318                     node_type="null"
319                     ctdb_node=0
320                 fi
321             fi
322         }
323
324         hack_all_nodes_with hack_filter
325                         
326         [ $numnodes -gt 0 ] && \
327             die "Can't not use NUMNODES to increase the number of nodes over that specified by NODES.  You need to set NODES instead - please read the documentation."
328     fi
329     
330     # Check IP addresses for duplicates.
331     local ip_offsets=":"
332     # This function doesn't modify anything...
333     get_ip_offset ()
334     {
335         [ "${ip_offsets/${ip_offset}}" != "$ip_offsets" ] && \
336             die "Duplicate IP offset in NODES - ${node_type}:${ip_offset}"
337         ip_offsets="${ip_offsets}${ip_offset}:"
338     }
339     hack_all_nodes_with get_ip_offset
340 }
341
342 ##############################
343
344 sanity_check_cluster_name ()
345 {
346     [ -z "${CLUSTER//[A-Za-z0-9]}" ] || \
347         die "Cluster names should be restricted to the characters A-Za-z0-9.  \
348 Some cluster filesystems have problems with other characters."
349 }
350
351 hosts_file=
352
353 common_nodelist_hacking ()
354 {
355     # Rework the NODES list
356     expand_nodes
357
358     # Build /etc/hosts and hack the names of the ctdb nodes
359     hosts_line_hack_name ()
360     {
361         # Ignore nodes without names (e.g. "null")
362         [ "$node_type" != "null" -a -n "$name" ] || return 0
363
364         local sname=""
365         local hosts_line
366         local ip_addr="${NETWORK_PRIVATE_PREFIX}.$(($FIRSTIP + $ip_offset))"
367         
368         if [ "$ctdb_node" = 1 ] ; then
369             num_ctdb_nodes=$(($num_ctdb_nodes + 1))
370             sname="${CLUSTER}n${num_ctdb_nodes}"
371             hosts_line="$ip_addr ${sname}.${ld} ${name}.${ld} $name $sname"
372             name="$sname"
373         else
374             hosts_line="$ip_addr ${name}.${ld} $name"
375         fi
376
377         # This allows you to add a function to your configuration file
378         # to modify hostnames (and other aspects of nodes).  This
379         # function can access/modify $name (the existing name),
380         # $node_type and $ctdb_node (1, if the node is a member of the
381         # CTDB cluster, 0 otherwise).
382         if [ -n "$HOSTNAME_HACKING_FUNCTION" ] ; then
383             local old_name="$name"
384             $HOSTNAME_HACKING_FUNCTION
385             if [ "$name" != "$old_name" ] ; then
386                 hosts_line="$ip_addr ${name}.${ld} $name"
387             fi
388         fi
389
390         echo "$hosts_line"
391     }
392     hosts_file="tmp/hosts.$CLUSTER"
393     {
394         local num_ctdb_nodes=0
395         local ld=$(echo $DOMAIN | tr A-Z a-z)
396         echo "# autocluster $CLUSTER"
397         hack_all_nodes_with hosts_line_hack_name
398         echo
399     } >$hosts_file
400
401     # Build /etc/ctdb/nodes
402     ctdb_nodes_line ()
403     {
404         [ "$ctdb_node" = 1 ] || return 0
405         echo "${NETWORK_PRIVATE_PREFIX}.$(($FIRSTIP + $ip_offset))"
406         num_nodes=$(($num_nodes + 1))
407     }
408     nodes_file="tmp/nodes.$CLUSTER"
409     local num_nodes=0
410     hack_all_nodes_with ctdb_nodes_line >$nodes_file
411     : "${NUMNODES:=${num_nodes}}"  # Set $NUMNODES if necessary
412
413     # Build UUID map
414     uuid_map="tmp/uuid_map.$CLUSTER"
415     uuid_map_line ()
416     {
417         echo "${ip_offset} $(uuidgen) ${node_type}"
418     }
419     hack_all_nodes_with uuid_map_line >$uuid_map
420 }
421
422 create_cluster_hooks=
423 cluster_created_hooks=
424
425 create_cluster ()
426 {
427     CLUSTER="$1"
428
429     sanity_check_cluster_name
430
431     mkdir -p $VIRTBASE/$CLUSTER $KVMLOG tmp
432
433     # Run hooks before doing anything else.
434     run_hooks create_cluster_hooks
435
436     common_nodelist_hacking
437
438     for_each_node call_func create_node
439
440     echo "Cluster $CLUSTER created"
441     echo ""
442
443     run_hooks cluster_created_hooks
444 }
445
446 cluster_created_hosts_message ()
447 {
448     echo "You may want to add this to your /etc/hosts file:"
449     cat $hosts_file
450 }
451
452 register_hook cluster_created_hooks cluster_created_hosts_message
453
454 create_one_node ()
455 {
456     CLUSTER="$1"
457     local single_node_ip_offset="$2"
458
459     sanity_check_cluster_name
460
461     mkdir -p $VIRTBASE/$CLUSTER $KVMLOG tmp
462
463     common_nodelist_hacking
464
465     for n in $NODES ; do
466         set -- $(IFS=: ; echo $n)
467         [ $single_node_ip_offset -eq $2 ] || continue
468         call_func create_node "$@"
469         
470         echo "Requested node created"
471         echo ""
472         echo "You may want to update your /etc/hosts file:"
473         cat $hosts_file
474         
475         break
476     done
477 }
478
479 ###############################
480 # test the proxy setup
481 test_proxy() {
482     export http_proxy=$WEBPROXY
483     wget -O /dev/null $INSTALL_SERVER || \
484         die "Your WEBPROXY setting \"$WEBPROXY\" is not working"
485     echo "Proxy OK"
486 }
487
488 ###################
489
490 kickstart_floppy_create_hooks=
491
492 # create base image
493 create_base()
494 {
495     local NAME="$BASENAME"
496     local DISK="${VIRTBASE}/${NAME}.${BASE_FORMAT}"
497     run_hooks hack_disk_hooks "base"
498
499     mkdir -p $KVMLOG
500
501     echo "Testing WEBPROXY $WEBPROXY"
502     test_proxy
503
504     local di="$DISK"
505     if [ "$DISK_FOLLOW_SYMLINKS" = "yes" -a -L "$DISK" ] ; then
506         di=$(readlink "$DISK")
507     fi
508     rm -f "$di"
509     local di_dirname="${di%/*}"
510     mkdir -p "$di_dirname"
511
512     echo "Creating the disk"
513     qemu-img create -f $BASE_FORMAT "$di" $DISKSIZE
514
515     rm -rf tmp
516     mkdir -p mnt tmp tmp/ISO
517
518     setup_timezone
519
520     echo "Creating kickstart file from template"
521     substitute_vars "$KICKSTART" "tmp/ks.cfg"
522
523     if [ $INSTALLKEY = "--skip" ]; then
524         cat <<EOF
525 --------------------------------------------------------------------------------------
526 WARNING: You have not entered an install key. Some RHEL packages will not be installed.
527
528 Please enter a valid RHEL install key in your config file like this:
529
530   INSTALLKEY="1234-5678-0123-4567"
531
532 The install will continue without an install key in 5 seconds
533 --------------------------------------------------------------------------------------
534 EOF
535         sleep 5
536     fi
537
538     # $ISO gets $ISO_DIR prepended if it doesn't start with a leading '/'.
539     case "$ISO" in
540         (/*) : ;;
541         (*) ISO="${ISO_DIR}/${ISO}"
542     esac
543     
544     echo "Creating kickstart floppy"
545     dd if=/dev/zero of=tmp/floppy.img bs=1024 count=1440
546     mkdosfs tmp/floppy.img
547     mount -o loop -t msdos tmp/floppy.img mnt
548     cp tmp/ks.cfg mnt
549     mount -o loop,ro $ISO tmp/ISO
550     
551     echo "Setting up bootloader"
552     cp tmp/ISO/isolinux/isolinux.bin tmp
553     cp tmp/ISO/isolinux/vmlinuz tmp
554     cp tmp/ISO/isolinux/initrd.img tmp
555
556     run_hooks kickstart_floppy_create_hooks
557
558     umount tmp/ISO
559     umount mnt
560
561     UUID=`uuidgen`
562
563     substitute_vars $INSTALL_TEMPLATE tmp/$NAME.xml
564
565     rm -f $KVMLOG/serial.$NAME
566
567     # boot the install CD
568     $VIRSH create tmp/$NAME.xml
569
570     echo "Waiting for install to start"
571     sleep 2
572     
573     # wait for the install to finish
574     if ! waitfor $KVMLOG/serial.$NAME "$KS_DONE_MESSAGE" $CREATE_BASE_TIMEOUT ; then
575         $VIRSH destroy $NAME
576         die "Failed to create base image ${DISK} after waiting for ${CREATE_BASE_TIMEOUT} seconds.
577 You may need to increase the value of CREATE_BASE_TIMEOUT.
578 Alternatively, the install might have completed but KS_DONE_MESSAGE
579 (currently \"${KS_DONE_MESSAGE}\")
580 may not have matched anything at the end of the kickstart output."
581     fi
582     
583     $VIRSH destroy $NAME
584
585     ls -l $DISK
586     cat <<EOF
587
588 Install finished, base image $DISK created
589
590 You may wish to run
591    chattr +i $DISK
592 To ensure that this image does not change
593
594 Note that the root password has been set to $ROOTPASSWORD
595
596 EOF
597 }
598
599 ###############################
600 # boot the base disk
601 boot_base() {
602     CLUSTER="$1"
603
604     NAME="$BASENAME"
605     DISK="${VIRTBASE}/${NAME}.${BASE_FORMAT}"
606
607     rm -rf tmp
608     mkdir -p tmp
609
610     IPNUM=$FIRSTIP
611     CLUSTER="base"
612
613     diskimage mount $DISK
614     setup_base
615     diskimage unmount
616
617     UUID=`uuidgen`
618     
619     echo "Creating $NAME.xml"
620     substitute_vars $BOOT_TEMPLATE tmp/$NAME.xml
621     
622     # boot the base system
623     $VIRSH create tmp/$NAME.xml
624 }
625
626 ######################################################################
627
628 # Updating a disk image...
629
630 diskimage ()
631 {
632     local func="$1"
633     shift
634     call_func diskimage_"$func" "$SYSTEM_DISK_ACCESS_METHOD" "$@"
635 }
636
637 # setup the files from $BASE_TEMPLATES/, substituting any variables
638 # based on the config
639 copy_base_dir_substitute_templates ()
640 {
641     local dir="$1"
642
643     local d="$BASE_TEMPLATES/$dir"
644     [ -d "$d" ] || return 0
645
646     local f
647     for f in $(cd "$d" && find . \! -name '*~' \( -type d -name .svn -prune -o -print \) ) ; do
648         f="${f#./}" # remove leading "./" for clarity
649         if [ -d "$d/$f" ]; then
650             # Don't chmod existing directory
651             if diskimage is_directory "/$f" ; then
652                 continue
653             fi
654             diskimage mkdir_p "/$f"
655         else
656             echo " Install: $f"
657             diskimage substitute_vars "$d/$f" "/$f"
658         fi
659         diskimage chmod_reference "$d/$f" "/$f"
660     done
661 }
662
663 setup_base_hooks=
664
665 setup_base_ssh_keys ()
666 {
667     # this is needed as git doesn't store file permissions other
668     # than execute
669     # Note that we protect the wildcards from the local shell.
670     diskimage chmod 600 "/etc/ssh/*key" "/root/.ssh/*"
671     diskimage chmod 700 "/etc/ssh" "/root/.ssh" "/root"
672     if [ -r "$HOME/.ssh/id_rsa.pub" ]; then
673        echo "Adding $HOME/.ssh/id_rsa.pub to ssh authorized_keys"
674        diskimage append_text_file "$HOME/.ssh/id_rsa.pub" "/root/.ssh/authorized_keys"
675     fi
676     if [ -r "$HOME/.ssh/id_dsa.pub" ]; then
677        echo "Adding $HOME/.ssh/id_dsa.pub to ssh authorized_keys"
678        diskimage append_text_file "$HOME/.ssh/id_dsa.pub" "/root/.ssh/authorized_keys"
679     fi
680 }
681
682 register_hook setup_base_hooks setup_base_ssh_keys
683
684 setup_base_grub_conf ()
685 {
686     echo "Adjusting grub.conf"
687     local o="$EXTRA_KERNEL_OPTIONS" # For readability.
688     diskimage sed "/boot/grub/grub.conf" \
689         -e "s/console=ttyS0,19200/console=ttyS0,115200/"  \
690         -e "s/ nodmraid//" -e "s/ nompath//"  \
691         -e "s/quiet/noapic divider=10${o:+ }${o}/g"
692 }
693
694 register_hook setup_base_hooks setup_base_grub_conf
695
696 setup_base()
697 {
698     local type="$1"
699
700     umask 022
701     echo "Copy base files"
702     copy_base_dir_substitute_templates "all"
703     if [ -n "$type" ] ; then
704         copy_base_dir_substitute_templates "$type"
705     fi
706
707     run_hooks setup_base_hooks
708 }
709
710 # setup various networking components
711 setup_network()
712 {
713     # This avoids doing anything when we're called from boot_base().
714     if [ -z "$hosts_file" ] ; then
715         echo "Skipping network-related setup"
716         return
717     fi
718
719     echo "Setting up networks"
720     diskimage append_text_file "$hosts_file" "/etc/hosts"
721
722     echo "Setting up /etc/ctdb/nodes"
723     diskimage mkdir_p "/etc/ctdb"
724     diskimage put "$nodes_file" "/etc/ctdb/nodes"
725
726     [ "$WEBPROXY" = "" ] || {
727         diskimage append_text "export http_proxy=$WEBPROXY" "/etc/bashrc"
728     }
729
730     if [ -n "$NFSSHARE" -a -n "$NFS_MOUNTPOINT" ] ; then
731         echo "Enabling nfs mount of $NFSSHARE"
732         diskimage mkdir_p "$NFS_MOUNTPOINT"
733         diskimage append_text "$NFSSHARE $NFS_MOUNTPOINT nfs nfsvers=3,intr 0 0" "/etc/fstab"
734     fi
735
736     diskimage mkdir_p "/etc/yum.repos.d"
737     echo '@@@YUM_TEMPLATE@@@' | diskimage substitute_vars - "/etc/yum.repos.d/autocluster.repo"
738
739     diskimage rm_rf "/etc/udev/rules.d/70-persistent-net.rules"
740
741     echo "Setting up network interfaces: "
742     local n
743     for n in $NETWORKS ; do
744         local dev="${n#*,}" # Strip address, comma
745         dev="${dev%,*}" # Strip comma, interface
746         echo "  $dev"
747         cat <<EOF | \
748             diskimage substitute_vars \
749             - "/etc/sysconfig/network-scripts/ifcfg-${dev}"
750 DEVICE=$dev
751 ONBOOT=yes
752 TYPE=Ethernet
753 IPADDR=${n%.*}.@@IPNUM@@
754 NETMASK=255.255.255.0
755 EOF
756     done
757 }
758
759 register_hook setup_base_hooks setup_network
760
761 setup_timezone() {
762     [ -z "$TIMEZONE" ] && {
763         [ -r /etc/timezone ] && {
764             TIMEZONE=`cat /etc/timezone`
765         }
766         [ -r /etc/sysconfig/clock ] && {
767             . /etc/sysconfig/clock
768             TIMEZONE="$ZONE"
769         }
770         TIMEZONE="${TIMEZONE// /_}"
771     }
772     [ -n "$TIMEZONE" ] || \
773         die "Unable to determine TIMEZONE - please set in config"
774 }
775
776 # substite a set of variables of the form @@XX@@ for the shell
777 # variables $XX in a file.
778 #
779 # Indirect variables @@@XX@@@ (3 ats) specify that the variable should
780 # contain a filename whose contents are substituted, with variable
781 # substitution applied to those contents.  If filename starts with '|'
782 # it is a command instead - however, quoting is extremely fragile.
783 substitute_vars() {(
784         infile="${1:-/dev/null}" # if empty then default to /dev/null
785         outfile="$2" # optional
786
787         tmp_out=$(mktemp)
788         cat "$infile" >"$tmp_out"
789
790         # Handle any indirects by looping until nothing changes.
791         # However, only handle 10 levels of recursion.
792         count=0
793         while : ; do
794             if ! _substitute_vars "$tmp_out" "@@@" ; then
795                 rm -f "$tmp_out"
796                 die "Failed to expand template $infile"
797             fi
798
799             # No old version of file means no changes made.
800             if [ ! -f "${tmp_out}.old" ] ; then
801                 break
802             fi
803
804             rm -f "${tmp_out}.old"
805
806             count=$(($count + 1))
807             if [ $count -ge 10 ] ; then
808                 rm -f "$tmp_out"
809                 die "Recursion too deep in $infile - only 10 levels allowed!"
810             fi
811         done
812
813         # Now regular variables.
814         if ! _substitute_vars "$tmp_out" "@@" ; then
815             rm -f "$tmp_out"
816             die "Failed to expand template $infile"
817         fi
818         rm -f "${tmp_out}.old"
819
820         if [ -n "$outfile" ] ; then
821             mv "$tmp_out" "$outfile"
822         else
823             cat "$tmp_out"
824             rm -f "$tmp_out"
825         fi
826 )}
827
828
829 # Delimiter @@ means to substitute contents of variable.
830 # Delimiter @@@ means to substitute contents of file named by variable.
831 # @@@ supports leading '|' in variable value, which means to excute a
832 # command.
833 _substitute_vars() {(
834         tmp_out="$1"
835         delimiter="${2:-@@}"
836
837         # Get the list of variables used in the template.  The grep
838         # gets rid of any blank lines and lines with extraneous '@'s
839         # next to template substitutions.
840         VARS=$(sed -n -e "s#[^@]*${delimiter}\([A-Z0-9_][A-Z0-9_]*\)${delimiter}[^@]*#\1\n#gp" "$tmp_out" |
841             grep '^[A-Z0-9_][A-Z0-9_]*$' |
842             sort -u)
843
844         tmp=$(mktemp)
845         for v in $VARS; do
846             # variable variables are fun .....
847             [ "${!v+x}" ] || {
848                 rm -f $tmp
849                 die "No substitution given for ${delimiter}$v${delimiter} in $infile"
850             }
851             s=${!v}
852
853             if [ "$delimiter" = "@@@" ] ; then
854                 f=${s:-/dev/null}
855                 c="${f#|}" # Is is a command, signified by a leading '|'?
856                 if [ "$c" = "$f" ] ; then
857                     # No leading '|', cat file.
858                     s=$(cat -- "$f")
859                     [ $? -eq 0 ] || {
860                         rm -f $tmp
861                         die "Could not substitute contents of file $f"
862                     }
863                 else
864                     # Leading '|', execute command.
865                     # Quoting problems here - using eval "$c" doesn't help.
866                     s=$($c)
867                     [ $? -eq 0 ] || {
868                         rm -f $tmp
869                         die "Could not execute command $c"
870                     }
871                 fi
872             fi
873
874             # escape some pesky chars
875             # This first one can be too slow if done using a bash
876             # variable pattern subsitution.
877             s=$(echo -n "$s" | tr '\n' '\001' | sed -e 's/\o001/\\n/g')
878             s=${s//#/\\#}
879             s=${s//&/\\&}
880             echo "s#${delimiter}${v}${delimiter}#${s}#g"
881         done > $tmp
882
883         # Get the in-place sed to make a backup of the old file.
884         # Remove the backup if it is the same as the resulting file -
885         # this acts as a flag to the caller that no changes were made.
886         sed -i.old -f $tmp "$tmp_out"
887         if cmp -s "${tmp_out}.old" "$tmp_out" ; then
888             rm -f "${tmp_out}.old"
889         fi
890
891         rm -f $tmp
892 )}
893
894 check_command() {
895     which $1 > /dev/null || die "Please install $1 to continue"
896 }
897
898 # Set a variable if it isn't already set.  This allows environment
899 # variables to override default config settings.
900 defconf() {
901     local v="$1"
902     local e="$2"
903
904     [ "${!v+x}" ] || eval "$v=\"$e\""
905 }
906
907 load_config () {
908     local i
909
910     for i in "${installdir}/config.d/"*.defconf ; do
911         . "$i"
912     done
913 }
914
915 # Print the list of config variables defined in config.d/.
916 get_config_options () {( # sub-shell for local declaration of defconf()
917         local options=
918         defconf() { options="$options $1" ; }
919         load_config
920         echo $options
921 )}
922
923 # Produce a list of long options, suitable for use with getopt, that
924 # represent the config variables defined in config.d/.
925 getopt_config_options () {
926     local x=$(get_config_options | tr 'A-Z_' 'a-z-')
927     echo "${x// /:,}:"
928 }
929
930 # Unconditionally set the config variable associated with the given
931 # long option.
932 setconf_longopt () {
933     local longopt="$1"
934     local e="$2"
935
936     local v=$(echo "${longopt#--}" | tr 'a-z-' 'A-Z_')
937     # unset so defconf will set it
938     eval "unset $v"
939     defconf "$v" "$e"
940 }
941
942 # Dump all of the current config variables.
943 dump_config() {
944     local o
945     for o in $(get_config_options) ; do
946         echo "${o}=\"${!o}\""
947     done
948     exit 0
949 }
950
951 # $COLUMNS is set in interactive bash shells.  It probably isn't set
952 # in this shell, so let's set it if it isn't.
953 : ${COLUMNS:=$(stty size 2>/dev/null | sed -e 's@.* @@')}
954 : ${COLUMNS:=80}
955 export COLUMNS
956
957 # Print text assuming it starts after other text in $startcol and
958 # needs to wrap before $COLUMNS - 2.  Subsequent lines start at $startcol.
959 # Long "words" will extend past $COLUMNS - 2.
960 fill_text() {
961     local startcol="$1"
962     local text="$2"
963
964     local width=$(($COLUMNS - 2 - $startcol))
965     [ $width -lt 0 ] && width=$((78 - $startcol))
966
967     local out=""
968
969     local padding
970     if [ $startcol -gt 0 ] ; then
971         padding=$(printf "\n%${startcol}s" " ")
972     else
973         padding="
974 "
975     fi
976
977     while [ -n "$text" ] ; do
978         local orig="$text"
979
980         # If we already have output then arrange padding on the next line.
981         [ -n "$out" ] && out="${out}${padding}"
982
983         # Break the text at $width.
984         out="${out}${text:0:${width}}"
985         text="${text:${width}}"
986
987         # If we have left over text then the line break may be ugly,
988         # so let's check and try to break it on a space.
989         if [ -n "$text" ] ; then
990             # The 'x's stop us producing a special character like '(',
991             # ')' or '!'.  Yuck - there must be a better way.
992             if [ "x${text:0:1}" != "x " -a "x${text: -1:1}" != "x " ] ; then
993                 # We didn't break on a space.  Arrange for the
994                 # beginning of the broken "word" to appear on the next
995                 # line but not if it will make us loop infinitely.
996                 if [ "${orig}" != "${out##* }${text}" ] ; then
997                     text="${out##* }${text}"
998                     out="${out% *}"
999                 else
1000                     # Hmmm, doing that would make us loop, so add the
1001                     # rest of the word from the remainder of the text
1002                     # to this line and let it extend past $COLUMNS - 2.
1003                     out="${out}${text%% *}"
1004                     if [ "${text# *}" != "$text" ] ; then
1005                         # Remember the text after the next space for next time.
1006                         text="${text# *}"
1007                     else
1008                         # No text after next space.
1009                         text=""
1010                     fi
1011                 fi
1012             else
1013                 # We broke on a space.  If it will be at the beginning
1014                 # of the next line then remove it.
1015                 text="${text# }"
1016             fi
1017         fi
1018     done
1019
1020     echo "$out"
1021 }
1022
1023 # Display usage text, trying these approaches in order.
1024 # 1. See if it all fits on one line before $COLUMNS - 2.
1025 # 2. See if splitting before the default value and indenting it
1026 #    to $startcol means that nothing passes $COLUMNS - 2.
1027 # 3. Treat the message and default value as a string and just us fill_text()
1028 #    to format it. 
1029 usage_display_text () {
1030     local startcol="$1"
1031     local desc="$2"
1032     local default="$3"
1033     
1034     local width=$(($COLUMNS - 2 - $startcol))
1035     [ $width -lt 0 ] && width=$((78 - $startcol))
1036
1037     default="(default \"$default\")"
1038
1039     if [ $((${#desc} + 1 + ${#default})) -le $width ] ; then
1040         echo "${desc} ${default}"
1041     else
1042         local padding=$(printf "%${startcol}s" " ")
1043
1044         if [ ${#desc} -lt $width -a ${#default} -lt $width ] ; then
1045             echo "$desc"
1046             echo "${padding}${default}"
1047         else
1048             fill_text $startcol "${desc} ${default}"
1049         fi
1050     fi
1051 }
1052
1053 # Display usage information for long config options.
1054 usage_smart_display () {( # sub-shell for local declaration of defconf()
1055         local startcol=33
1056
1057         defconf() {
1058             local local longopt=$(echo "$1" | tr 'A-Z_' 'a-z-')
1059
1060             printf "     --%-25s " "${longopt}=${3}"
1061
1062             usage_display_text $startcol "$4" "$2"
1063         }
1064
1065         "$@"
1066 )}
1067
1068
1069 # Display usage information for long config options.
1070 usage_config_options (){
1071     usage_smart_display load_config
1072 }
1073
1074 list_releases () {
1075     local releases=$(cd $installdir/releases && echo *.release)
1076     releases="${releases//.release}"
1077     releases="${releases// /\", \"}"
1078     echo "\"$releases\""
1079 }
1080
1081 ######################################################################
1082
1083 post_config_hooks=
1084
1085 ######################################################################
1086
1087 load_config
1088
1089 ############################
1090 # parse command line options
1091 long_opts=$(getopt_config_options)
1092 getopt_output=$(getopt -n autocluster -o "c:e:E:xh" -l help,dump,with-release: -l "$long_opts" -- "$@")
1093 [ $? != 0 ] && usage
1094
1095 use_default_config=true
1096
1097 # We do 2 passes of the options.  The first time we just handle usage
1098 # and check whether -c is being used.
1099 eval set -- "$getopt_output"
1100 while true ; do
1101     case "$1" in
1102         -c) shift 2 ; use_default_config=false ;;
1103         -e) shift 2 ;;
1104         -E) shift 2 ;;
1105         --) shift ; break ;;
1106         --with-release) shift 2 ;; # Don't set use_default_config=false!!!
1107         --dump|-x) shift ;;
1108         -h|--help) usage ;; # Usage should be shown here for real defaults.
1109         --*) shift 2 ;; # Assume other long opts are valid and take an arg.
1110         *) usage ;; # shouldn't happen, so this is reasonable.
1111     esac
1112 done
1113
1114 config="./config"
1115 $use_default_config && [ -r "$config" ] && . "$config"
1116
1117 eval set -- "$getopt_output"
1118
1119 while true ; do
1120     case "$1" in
1121         # force at least ./local_file to avoid accidental file from $PATH
1122         -c) . "$(dirname $2)/$(basename $2)" ; shift 2 ;;
1123         -e) run_hooks post_config_hooks ; eval "$2" ; exit ;;
1124         -E) eval "$2" ; shift 2 ;;
1125         -x) set -x; shift ;;
1126         --dump) run_hooks post_config_hooks ; dump_config ;;
1127         --) shift ; break ;;
1128         -h|--help) usage ;; # Redundant.
1129         --*)
1130             # Putting --opt1|opt2|... into a variable and having case
1131             # match against it as a pattern doesn't work.  The | is
1132             # part of shell syntax, so we need to do this.  Look away
1133             # now to stop your eyes from bleeding! :-)
1134             x=",${long_opts}" # Now each option is surrounded by , and :
1135             if [ "$x" != "${x#*,${1#--}:}" ] ; then
1136                 # Our option, $1, surrounded by , and : was in $x, so is legal.
1137                 setconf_longopt "$1" "$2"; shift 2
1138             else
1139                 usage
1140             fi
1141             ;;
1142         *) usage ;; # shouldn't happen, so this is reasonable.
1143     esac
1144 done
1145
1146 run_hooks post_config_hooks 
1147
1148 # catch errors
1149 set -e
1150 set -E
1151 trap 'es=$?; 
1152       echo ERROR: failed in function \"${FUNCNAME}\" at line ${LINENO} of ${BASH_SOURCE[0]} with code $es; 
1153       exit $es' ERR
1154
1155 # check for needed programs 
1156 check_command expect
1157
1158 [ $# -lt 1 ] && usage
1159
1160 command="$1"
1161 shift
1162
1163 case $command in
1164     create)
1165         type=$1
1166         shift
1167         case $type in
1168             base)
1169                 [ $# != 0 ] && usage
1170                 create_base
1171                 ;;
1172             cluster)
1173                 [ $# != 1 ] && usage
1174                 create_cluster "$1"
1175                 ;;
1176             node)
1177                 [ $# != 2 ] && usage
1178                 create_one_node "$1" "$2"
1179                 ;;
1180             *)
1181                 usage;
1182                 ;;
1183         esac
1184         ;;
1185     mount)
1186         [ $# != 1 ] && usage
1187         diskimage mount "$1"
1188         ;;
1189     unmount|umount)
1190         [ $# != 0 ] && usage
1191         diskimage unmount
1192         ;;
1193     bootbase)
1194         boot_base;
1195         ;;
1196     testproxy)
1197         test_proxy;
1198         ;;
1199     *)
1200         usage;
1201         ;;
1202 esac