#!/bin/bash
#

# Copyright (C) 2012 Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

set -e -u -o pipefail
shopt -s extglob

readonly self=$(readlink -f $0)
readonly ensure_dirs=/usr/lib/ganeti/ensure-dirs
readonly action_shortcuts=( start stop restart status watcher )
readonly default_nodecount=5
readonly default_instcount=10
readonly default_netprefix=192.0.2
readonly default_netdev=eth0
readonly default_initscript=/etc/init.d/ganeti
readonly cluster_name=cluster
readonly etc_hosts_filename=/etc/hosts

# IP address space:
# Cluster: .1
# Nodes: .10-.99
# Instances: .100-.254
readonly first_node_ipaddr_octet=10
readonly first_inst_ipaddr_octet=100

readonly max_node_count=$((first_inst_ipaddr_octet - first_node_ipaddr_octet))
readonly max_instance_count=$((255 - first_inst_ipaddr_octet))

usage() {
  echo "Usage: $0 [-E] [-N] [-c <number>] [-i <number>] [-p <prefix>]"\
       '[-n <netdev>] [-I <path>] <directory>'
  echo
  echo 'Options:'
  echo "  -c  Number of virtual nodes (defaults to $default_nodecount)"
  echo "  -i  Number of instances (defaults to $default_instcount)"
  echo "  -p  IPv4 network prefix (defaults to $default_netprefix)"
  echo '  -n  Network device for virtual IP addresses (defaults to'\
       "$default_netdev)"
  echo "  -I  Path to init script (defaults to $default_initscript)"
  echo "  -E  Do not modify $etc_hosts_filename"
  echo '  -N  Do not configure networking'
}

# Variables for options
nodecount=$default_nodecount
instcount=$default_instcount
netprefix=$default_netprefix
netdev=$default_netdev
initscript=$default_initscript
etchosts=1
networking=1

# Parse options
while getopts :hENc:p:n:i:I: opt; do
  case "$opt" in
    h)
      usage
      exit 0
    ;;
    c)
      nodecount="$OPTARG"
      if [[ "$nodecount" != +([0-9]) ]]; then
        echo "Invalid node count number: $nodecount" >&2
        exit 1
      elif (( nodecount > max_node_count )); then
        echo "Node count must be $max_node_count or lower" >&2
        exit 1
      fi
    ;;
    i)
      instcount="$OPTARG"
      if [[ "$instcount" != +([0-9]) ]]; then
        echo "Invalid instance count number: $instcount" >&2
        exit 1
      elif (( instcount > max_instance_count )); then
        echo "Instance count must be $max_instance_count or lower" >&2
        exit 1
      fi
    ;;
    p)
      netprefix="$OPTARG"
      if [[ "$netprefix" != +([0-9]).+([0-9]).+([0-9]) ]]; then
        echo "Invalid network prefix: $netprefix" >&2
        exit 1
      fi
    ;;
    n)
      netdev="$OPTARG"
      if ! ip link show $netdev >/dev/null; then
        echo "Invalid network device: $netdev" >&2
        exit 1
      fi
    ;;
    I)
      initscript="$OPTARG"
      if [[ ! -x $initscript ]]; then
        echo "Init script '$initscript' is not executable" >&2
        exit 1
      fi
      ;;
    E)
      etchosts=
      ;;
    N)
      networking=
      ;;
    \?)
      echo "Invalid option: -$OPTARG" >&2
      usage >&2
      exit 1
      ;;
    :)
      echo "Option -$OPTARG requires an argument" >&2
      usage >&2
      exit 1
      ;;
  esac
done

shift $((OPTIND - 1))

if [[ "$#" != 1 ]]; then
  usage
  exit 1
fi

readonly rootdir=$1; shift

if [[ ! -d "$rootdir" ]]; then
  echo "Directory '$rootdir' does not exist!" >&2
  exit 1
fi

if (( $nodecount < 1 )); then
  echo "Must create at least one node, currently requested $nodecount" >&2
  exit 1
fi

node_hostname() {
  local -r number="$1"

  echo "node$((number + 1))"
}

instance_hostname() {
  local -r number="$1"

  echo "instance$((number + 1))"
}

node_ipaddr() {
  local -r number="$1"

  echo "$netprefix.$((first_node_ipaddr_octet + number))"
}

instance_ipaddr() {
  local -r number="$1"

  echo "$netprefix.$((first_inst_ipaddr_octet + number))"
}

setup_node() {
  local -r number="$1"
  local -r nodedir=$rootdir/$(node_hostname $number)

  echo "Setting up node '$(node_hostname $number)' ..." >&2

  if [[ ! -d $nodedir ]]; then
    mkdir $nodedir
  fi

  mkdir -p \
    $nodedir/etc/{default,ganeti} \
    $nodedir/var/lock\
    $nodedir/var/{lib,log,run}/ganeti

  GANETI_HOSTNAME=$(node_hostname $number) \
  GANETI_ROOTDIR=$nodedir \
  $ensure_dirs

  local -r daemon_args="-b $(node_ipaddr $number)"

  cat > $nodedir/etc/default/ganeti <<EOF
# Default settings for virtual node $i
NODED_ARGS='--no-mlock $daemon_args'
MASTERD_ARGS=''
RAPI_ARGS='$daemon_args'
CONFD_ARGS='$daemon_args'
MOND_ARGS=''
WCONFD_ARGS=''
LUXID_ARGS=''
METAD_ARGS=''
KVMD_ARGS=''

export GANETI_ROOTDIR='$nodedir'
export GANETI_HOSTNAME='$(node_hostname $number)'
EOF

  cat > $nodedir/cmd <<EOF
#!/bin/bash

export GANETI_ROOTDIR='$nodedir'
export GANETI_HOSTNAME='$(node_hostname $number)'

bash -c "\$*"
EOF
  chmod +x $nodedir/cmd
}

setup_all_nodes() {
  for ((i=0; i < nodecount; ++i)); do
    setup_node $i
  done
}

setup_etc_hosts() {
  echo "Configuring $etc_hosts_filename ..." >&2
  (
    set -e -u
    local -r tmpfile=$(mktemp $etc_hosts_filename.vcluster.XXXXX)
    trap "rm -f $tmpfile" EXIT
    {
      egrep -v "^$netprefix.[[:digit:]]+[[:space:]]" $etc_hosts_filename
      echo "$netprefix.1 $cluster_name"
      for ((i=0; i < nodecount; ++i)); do
        echo "$(node_ipaddr $i) $(node_hostname $i)"
      done
      for ((i=0; i < instcount; ++i)); do
        echo "$(instance_ipaddr $i) $(instance_hostname $i)"
      done
    } > $tmpfile && \
    chmod 0644 $tmpfile && \
    mv $tmpfile $etc_hosts_filename && \
    trap - EXIT
  )
}

setup_network_interfaces() {
  echo 'Configuring network ...' >&2
  for ((i=0; i < nodecount; ++i)); do
    local ipaddr="$(node_ipaddr $i)/32"
    ip addr del "$ipaddr" dev "$netdev" || :
    ip addr add "$ipaddr" dev "$netdev"
  done

  route add -net $netprefix.0 netmask 255.255.255.0 dev $netdev || :
}

setup_scripts() {
  echo 'Configuring helper scripts ...' >&2
  for action in "${action_shortcuts[@]}"; do
    {
      echo '#!/bin/bash'
      for ((i=0; i < nodecount; ++i)); do
        local name=$(node_hostname $i)
        if [[ $action = watcher ]]; then
          echo "echo 'Running watcher for virtual node \"$name\" ..."
          echo "$name/cmd ganeti-watcher \"\$@\""
        else
          echo "echo 'Action \"$action\" for virtual node \"$name\" ...'"
          echo "$name/cmd $initscript $action \"\$@\""
        fi
      done
    } > $rootdir/$action-all
    chmod +x $rootdir/$action-all
  done
}

show_info() {
  cat <<EOF
Virtual cluster setup is complete.

Root directory: $rootdir
Cluster name: $cluster_name
EOF

  echo 'Nodes:' $(for ((i=0; i < nodecount; ++i)); do node_hostname $i; done)

  cat <<EOF

Initialize cluster:
  cd $rootdir && node1/cmd gnt-cluster init --no-etc-hosts \\
    --no-ssh-init --master-netdev=lo \\
    --enabled-disk-templates=diskless --enabled-hypervisors=fake \\
    --ipolicy-bounds-specs=min:disk-size=0,cpu-count=1,disk-count=0,memory-size=1,nic-count=0,spindle-use=0/max:disk-size=1048576,cpu-count=8,disk-count=16,memory-size=32768,nic-count=8,spindle-use=12 \\
    $cluster_name

Add node:
  cd $rootdir && node1/cmd gnt-node add --no-ssh-key-check node2
EOF
}

setup_all_nodes
if [[ -n "$etchosts" ]]; then
  setup_etc_hosts
fi
if [[ -n "$networking" ]]; then
  setup_network_interfaces
fi
setup_scripts
show_info

exit 0

# vim: set sw=2 sts=2 et :
