Always default _SHARED_DISK_TEMPLATE to ""
[autocluster.git] / autocluster
index df4ed57d76065e8d514afcbbf89c04e15efaaad1..94d942e9c3236db2b3a983a10ae394065ff8970d 100755 (executable)
@@ -52,10 +52,14 @@ EOF
     cat <<EOF
 
   commands:
+     base [ create | boot ] ...
+
+     cluster [ build | destroy | create | update_hosts | boot | setup ] ...
+
      create base
            create a base image
 
-     create cluster CLUSTERNAME
+     create cluster [ CLUSTERNAME ]
            create a full cluster
 
      create node CLUSTERNAME IP_OFFSET
@@ -69,9 +73,6 @@ EOF
 
      bootbase
            boot the base image
-
-     testproxy
-           test your proxy setup
 EOF
     exit 1
 }
@@ -87,6 +88,39 @@ die () {
     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
+}
+
 ###############################
 
 # Indirectly call a function named by ${1}_${2}
@@ -117,6 +151,11 @@ for_each_node ()
     done
 }
 
+node_is_ctdb_node_DEFAULT ()
+{
+    echo 0
+}
+
 hack_one_node_with ()
 {
     local filter="$1" ; shift
@@ -179,6 +218,18 @@ clear_hooks ()
 # the same disk.
 hack_disk_hooks=""
 
+create_node_DEFAULT ()
+{
+    local type="$1"
+    local ip_offset="$2"
+    local name="$3"
+    local ctdb_node="$4"
+
+    echo "Creating node \"$name\" (of type \"${type}\")"
+
+    create_node_COMMON "$name" "$ip_offset" "$type"
+}
+
 # common node creation stuff
 create_node_COMMON ()
 {
@@ -268,11 +319,6 @@ create_node_configure_image ()
     diskimage unmount
 }
 
-# Provides an easy way of removing nodes from $NODE.
-create_node_null () {
-    :
-}
-
 hack_network_map_hooks=""
 
 # Uses: CLUSTER, NAME, NETWORKS, FIRSTIP, ip_offset
@@ -317,9 +363,8 @@ make_network_map ()
 
 ##############################
 
-hack_nodes_functions=
-
-expand_nodes () {
+expand_nodes ()
+{
     # Expand out any abbreviations in NODES.
     local ns=""
     local n
@@ -345,35 +390,6 @@ expand_nodes () {
     done
     NODES="$ns"
 
-    # Apply nodes hacks.  Some of this is about backward compatibility
-    # but the hacks also fill in the node names and whether they're
-    # part of the CTDB cluster.  The order is the order that
-    # configuration modules register their hacks.
-    run_hooks hack_nodes_functions
-
-    if [ -n "$NUMNODES" ] ; then
-       # Attempt to respect NUMNODES.  Reduce the number of CTDB
-       # nodes to NUMNODES.
-       local numnodes=$NUMNODES
-
-       hack_filter ()
-       {
-           if [ "$ctdb_node" = 1 ] ; then
-               if [ $numnodes -gt 0 ] ; then
-                   numnodes=$(($numnodes - 1))
-               else
-                   node_type="null"
-                   ctdb_node=0
-               fi
-           fi
-       }
-
-       hack_all_nodes_with hack_filter
-                       
-       [ $numnodes -gt 0 ] && \
-           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."
-    fi
-    
     # Check IP addresses for duplicates.
     local ip_offsets=":"
     # This function doesn't modify anything...
@@ -384,6 +400,27 @@ expand_nodes () {
        ip_offsets="${ip_offsets}${ip_offset}:"
     }
     hack_all_nodes_with get_ip_offset
+
+    # Determine node names and whether they're in the CTDB cluster
+    declare -A node_count
+    _get_name_ctdb_node ()
+    {
+       local count=$((${node_count[$node_type]:-0} + 1))
+       node_count[$node_type]=$count
+       local fmt
+       fmt=$(call_func node_name_format "$node_type") || {
+           echo "ERROR: Node type \"${node_type}\" not defined!"
+           echo "Valid node types are:"
+           set | sed -n 's@^node_name_format_\(.*\) ().*@  \1@p'
+           exit 1
+       }
+       # printf behaves weirdly if given too many args for format, so
+       # "head" handles the case where there is no %d or similar for
+       # $count.
+       name=$(printf "${fmt}" "$CLUSTER" $count | head -n 1)
+       ctdb_node=$(call_func node_is_ctdb_node "$node_type")
+    }
+    hack_all_nodes_with _get_name_ctdb_node
 }
 
 ##############################
@@ -397,7 +434,7 @@ Some cluster filesystems have problems with other characters."
 
 hosts_file=
 
-common_nodelist_hacking ()
+cluster_nodelist_hacking ()
 {
     # Rework the NODES list
     expand_nodes
@@ -405,13 +442,11 @@ common_nodelist_hacking ()
     # Build /etc/hosts and hack the names of the ctdb nodes
     hosts_line_hack_name ()
     {
-       # Ignore nodes without names (e.g. "null")
-       [ "$node_type" != "null" -a -n "$name" ] || return 0
-
        local sname=""
        local hosts_line
        local ip_addr="${NETWORK_PRIVATE_PREFIX}.$(($FIRSTIP + $ip_offset))"
-       
+
+       # Primary name for CTDB nodes is <CLUSTER>n<num>
        if [ "$ctdb_node" = 1 ] ; then
            num_ctdb_nodes=$(($num_ctdb_nodes + 1))
            sname="${CLUSTER}n${num_ctdb_nodes}"
@@ -455,7 +490,6 @@ common_nodelist_hacking ()
     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"
@@ -469,9 +503,12 @@ common_nodelist_hacking ()
 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
 
@@ -480,8 +517,6 @@ create_cluster ()
     # Run hooks before doing anything else.
     run_hooks create_cluster_hooks
 
-    common_nodelist_hacking
-
     for_each_node call_func create_node
 
     echo "Cluster $CLUSTER created"
@@ -498,6 +533,147 @@ cluster_created_hosts_message ()
 
 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}\""
+
+    # Building a general node name regexp is a bit cumbersome.  :-)
+    local name_regexp="("
+    for i in $(set | sed -n -e "s@^\(node_name_format_.*\) ().*@\1@p") ; do
+       # For each node_name_format_* function, get the actual node
+       # name format string
+       fmt=$($i)
+       # fill it with placeholders (remembering that "_" is not valid
+       # in a real cluster name)
+       local t=$(printf "$fmt\n" "_" "0" | head -n 1)
+       # now replace the placeholders with regexps - order is
+       # important here, since the cluster name can contain digits
+       t=$(sed -r -e "s@[[:digit:]]+@[[:digit:]]+@" -e "s@_@${CLUSTER}@" <<<"$t")
+       # add to the regexp
+       name_regexp="${name_regexp}${t}|"
+    done
+    name_regexp="${name_regexp}${CLUSTER}n[[:digit:]]+)"
+
+    local pat="# autocluster ${CLUSTER}\$|[[:space:]]${name_regexp}"
+
+    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_setup_tasks_DEFAULT ()
+{
+    local stage="$1"
+
+    # By default nodes have no tasks
+    case "$stage" in
+       install_packages) echo "" ;;
+       setup_clusterfs)  echo "" ;;
+       setup_node)       echo "" ;;
+       setup_cluster)    echo "" ;;
+    esac
+}
+
+cluster_setup ()
+{
+    announce "cluster setup \"${CLUSTER}\""
+    [ -n "$CLUSTER" ] || die "\$CLUSTER not set"
+
+    local ssh="ssh -o StrictHostKeyChecking=no"
+    local setup_clusterfs_done=false
+    local setup_cluster_done=false
+
+    _cluster_setup_do_stage ()
+    {
+       local stage="$1"
+       local type="$2"
+       local ip_offset="$3"
+       local name="$4"
+       local ctdb_node="$5"
+
+       local tasks=$(call_func cluster_setup_tasks "$type" "$stage")
+
+       if [ -n "$tasks" ] ; then
+           # These tasks are only done on 1 node
+           case "$stage" in
+               setup_clusterfs)
+                   if $setup_clusterfs_done ; then
+                       return
+                   else
+                       setup_clusterfs_done=true
+                   fi
+                   ;;
+               setup_cluster)
+                   if $setup_cluster_done ; then
+                       return
+                   else
+                       setup_cluster_done=true
+                   fi
+                   ;;
+           esac
+
+           $ssh "$name" ./scripts/cluster_setup.sh "$stage" $tasks
+       fi
+
+    }
+
+    local stages="install_packages setup_clusterfs setup_node setup_cluster"
+    local stage
+    for stage in $stages ; do
+       for_each_node _cluster_setup_do_stage "$stage"
+    done
+}
+
 create_one_node ()
 {
     CLUSTER="$1"
@@ -507,8 +683,6 @@ create_one_node ()
 
     mkdir -p $VIRTBASE/$CLUSTER $KVMLOG tmp
 
-    common_nodelist_hacking
-
     for n in $NODES ; do
        set -- $(IFS=: ; echo $n)
        [ $single_node_ip_offset -eq $2 ] || continue
@@ -536,8 +710,29 @@ test_proxy() {
 
 kickstart_floppy_create_hooks=
 
+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"
+}
+
 # create base image
-create_base()
+base_create()
 {
     local NAME="$BASENAME"
     local DISK="${VIRTBASE}/${NAME}.${BASE_FORMAT}"
@@ -566,24 +761,11 @@ create_base()
 
     make_network_map
 
+    guess_install_network
+
     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:
-
-  INSTALLKEY="1234-5678-0123-4567"
-
-The install will continue without an install key in 5 seconds
---------------------------------------------------------------------------------------
-EOF
-       sleep 5
-    fi
-
     # $ISO gets $ISO_DIR prepended if it doesn't start with a leading '/'.
     case "$ISO" in
        (/*) : ;;
@@ -592,7 +774,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
@@ -637,6 +819,7 @@ may not have matched anything at the end of the kickstart output."
 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
 
@@ -647,7 +830,7 @@ EOF
 
 ###############################
 # boot the base disk
-boot_base() {
+base_boot() {
     rm -rf tmp
     mkdir -p tmp
 
@@ -735,10 +918,18 @@ setup_base_grub_conf ()
 {
     echo "Adjusting grub.conf"
     local o="$EXTRA_KERNEL_OPTIONS" # For readability.
-    diskimage sed "/boot/grub/grub.conf" \
-       -e "s/console=ttyS0,19200/console=ttyS0,115200/"  \
-       -e "s/ nodmraid//" -e "s/ nompath//"  \
-       -e "s/quiet/noapic divider=10${o:+ }${o}/g"
+    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
@@ -792,17 +983,35 @@ setup_network()
     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 substitute_vars \
-           - "/etc/sysconfig/network-scripts/ifcfg-${dev}"
+           diskimage put - "/etc/sysconfig/network-scripts/ifcfg-${dev}"
 DEVICE=$dev
 ONBOOT=yes
 TYPE=Ethernet
 IPADDR=$ip
 NETMASK=$mask
 HWADDR=$mac
+${gw:+GATEWAY=}${gw}
 EOF
-    done <"$network_map"
+
+       # 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
@@ -1120,11 +1329,24 @@ usage_config_options (){
     usage_smart_display load_config
 }
 
-list_releases () {
-    local releases=$(cd $installdir/releases && echo *.release)
-    releases="${releases//.release}"
-    releases="${releases// /\", \"}"
-    echo "\"$releases\""
+actions_init ()
+{
+    actions=""
+}
+
+actions_add ()
+{
+    actions="${actions}${actions:+ }$*"
+}
+
+actions_run ()
+{
+    [ -n "$actions" ] || usage
+
+    local a
+    for a in $actions ; do
+       $a
+    done
 }
 
 ######################################################################
@@ -1138,7 +1360,7 @@ load_config
 ############################
 # parse command line options
 long_opts=$(getopt_config_options)
-getopt_output=$(getopt -n autocluster -o "c:e: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
@@ -1152,7 +1374,6 @@ while true ; do
        -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.
@@ -1167,8 +1388,20 @@ 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 ;;
+       -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 ;;
@@ -1206,21 +1439,50 @@ check_command expect
 
 [ $# -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|setup)
+                   actions_add "cluster_${t}" ;;
+               build)
+                   for t in destroy create update_hosts boot setup ; do
+                       actions_add "cluster_${t}"
+                   done
+                   ;;
+               *) usage ;;
+           esac
+       done
+       cluster_nodelist_hacking
+       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
@@ -1240,10 +1502,7 @@ case $command in
        diskimage unmount
        ;;
     bootbase)
-       boot_base;
-       ;;
-    testproxy)
-       test_proxy;
+       base_boot;
        ;;
     *)
        usage;