#!/bin/bash
#
# A script to boot a dev swarm cluster on a Linux host (typically in a Docker
# container started with swarm/dev/run.sh).
#
# The cluster contains a bootnode, a energi3 node and multiple swarm nodes, with
# each node having its own data directory in a base directory passed with the
# --dir flag (default is swarm/dev/cluster).
#
# To avoid using different ports for each node and to make networking more
# realistic, each node gets its own network namespace with IPs assigned from
# the 192.168.33.0/24 subnet:
#
# bootnode: 192.168.33.2
# energi3:     192.168.33.3
# swarm:    192.168.33.10{1,2,...,n}

set -e

ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
source "${ROOT}/swarm/dev/scripts/util.sh"

# DEFAULT_BASE_DIR is the default base directory to store node data
DEFAULT_BASE_DIR="${ROOT}/swarm/dev/cluster"

# DEFAULT_CLUSTER_SIZE is the default swarm cluster size
DEFAULT_CLUSTER_SIZE=3

# Linux bridge configuration for connecting the node network namespaces
BRIDGE_NAME="swarmbr0"
BRIDGE_IP="192.168.33.1"

# static bootnode configuration
BOOTNODE_IP="192.168.33.2"
BOOTNODE_PORT="30301"
BOOTNODE_KEY="32078f313bea771848db70745225c52c00981589ad6b5b49163f0f5ee852617d"
BOOTNODE_PUBKEY="760c4460e5336ac9bbd87952a3c7ec4363fc0a97bd31c86430806e287b437fd1b01abc6e1db640cf3106b520344af1d58b00b57823db3e1407cbc433e1b6d04d"
BOOTNODE_URL="enode://${BOOTNODE_PUBKEY}@${BOOTNODE_IP}:${BOOTNODE_PORT}"

# static energi3 configuration
GETH_IP="192.168.33.3"
GETH_RPC_PORT="39796"
GETH_RPC_URL="http://${GETH_IP}:${GETH_RPC_PORT}"

usage() {
  cat >&2 <<USAGE
usage: $0 [options]

Boot a dev swarm cluster.

OPTIONS:
  -d, --dir DIR     Base directory to store node data [default: ${DEFAULT_BASE_DIR}]
  -s, --size SIZE   Size of swarm cluster [default: ${DEFAULT_CLUSTER_SIZE}]
  -h, --help        Show this message
USAGE
}

main() {
  local base_dir="${DEFAULT_BASE_DIR}"
  local cluster_size="${DEFAULT_CLUSTER_SIZE}"

  parse_args "$@"

  local pid_dir="${base_dir}/pids"
  local log_dir="${base_dir}/logs"
  mkdir -p "${base_dir}" "${pid_dir}" "${log_dir}"

  stop_cluster
  create_network
  start_bootnode
  start_energi3_node
  start_swarm_nodes
}

parse_args() {
  while true; do
    case "$1" in
      -h | --help)
        usage
        exit 0
        ;;
      -d | --dir)
        if [[ -z "$2" ]]; then
          fail "--dir flag requires an argument"
        fi
        base_dir="$2"
        shift 2
        ;;
      -s | --size)
        if [[ -z "$2" ]]; then
          fail "--size flag requires an argument"
        fi
        cluster_size="$2"
        shift 2
        ;;
      *)
        break
        ;;
    esac
  done

  if [[ $# -ne 0 ]]; then
    usage
    fail "ERROR: invalid arguments: $@"
  fi
}

stop_cluster() {
  info "stopping existing cluster"
  "${ROOT}/swarm/dev/scripts/stop-cluster.sh" --dir "${base_dir}"
}

# create_network creates a Linux bridge which is used to connect the node
# network namespaces together
create_network() {
  local subnet="${BRIDGE_IP}/24"

  info "creating ${subnet} network on ${BRIDGE_NAME}"
  ip link add name "${BRIDGE_NAME}" type bridge
  ip link set dev "${BRIDGE_NAME}" up
  ip address add "${subnet}" dev "${BRIDGE_NAME}"
}

# start_bootnode starts a bootnode which is used to bootstrap the energi3 and
# swarm nodes
start_bootnode() {
  local key_file="${base_dir}/bootnode.key"
  echo -n "${BOOTNODE_KEY}" > "${key_file}"

  local args=(
    --addr      "${BOOTNODE_IP}:${BOOTNODE_PORT}"
    --nodekey   "${key_file}"
    --verbosity "6"
  )

  start_node "bootnode" "${BOOTNODE_IP}" "$(which bootnode)" ${args[@]}
}

# start_energi3_node starts a energi3 node with --datadir pointing at <base-dir>/energi3
# and a single, unlocked account with password "energi3"
start_energi3_node() {
  local dir="${base_dir}/energi3"
  mkdir -p "${dir}"

  local password="energi3"
  echo "${password}" > "${dir}/password"

  # create an account if necessary
  if [[ ! -e "${dir}/keystore" ]]; then
    info "creating energi3 account"
    create_account "${dir}" "${password}"
  fi

  # get the account address
  local address="$(jq --raw-output '.address' ${dir}/keystore/*)"
  if [[ -z "${address}" ]]; then
    fail "failed to get energi3 account address"
  fi

  local args=(
    --datadir   "${dir}"
    --networkid "321"
    --bootnodes "${BOOTNODE_URL}"
    --unlock    "${address}"
    --password  "${dir}/password"
    --rpc
    --rpcaddr   "${GETH_IP}"
    --rpcport   "${GETH_RPC_PORT}"
    --verbosity "6"
  )

  start_node "energi3" "${GETH_IP}" "$(which energi3)" ${args[@]}
}

start_swarm_nodes() {
  for i in $(seq 1 ${cluster_size}); do
    start_swarm_node "${i}"
  done
}

# start_swarm_node starts a swarm node with a name like "swarmNN" (where NN is
# a zero-padded integer like "07"), --datadir pointing at <base-dir>/<name>
# (e.g. <base-dir>/swarm07) and a single account with <name> as the password
start_swarm_node() {
  local num=$1
  local name="swarm$(printf '%02d' ${num})"
  local ip="192.168.33.1$(printf '%02d' ${num})"

  local dir="${base_dir}/${name}"
  mkdir -p "${dir}"

  local password="${name}"
  echo "${password}" > "${dir}/password"

  # create an account if necessary
  if [[ ! -e "${dir}/keystore" ]]; then
    info "creating account for ${name}"
    create_account "${dir}" "${password}"
  fi

  # get the account address
  local address="$(jq --raw-output '.address' ${dir}/keystore/*)"
  if [[ -z "${address}" ]]; then
    fail "failed to get swarm account address"
  fi

  local args=(
    --bootnodes    "${BOOTNODE_URL}"
    --datadir      "${dir}"
    --identity     "${name}"
    --ens-api      "${GETH_RPC_URL}"
    --bzznetworkid "321"
    --bzzaccount   "${address}"
    --password     "${dir}/password"
    --verbosity    "6"
  )

  start_node "${name}" "${ip}" "$(which swarm)" ${args[@]}
}

# start_node runs the node command as a daemon in a network namespace
start_node() {
  local name="$1"
  local ip="$2"
  local path="$3"
  local cmd_args=${@:4}

  info "starting ${name} with IP ${ip}"

  create_node_network "${name}" "${ip}"

  # add a marker to the log file
  cat >> "${log_dir}/${name}.log" <<EOF

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Starting ${name} node - $(date)
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

EOF

  # run the command in the network namespace using start-stop-daemon to
  # daemonise the process, sending all output to the log file
  local daemon_args=(
    --start
    --background
    --no-close
    --make-pidfile
    --pidfile "${pid_dir}/${name}.pid"
    --exec "${path}"
  )
  if ! ip netns exec "${name}" start-stop-daemon ${daemon_args[@]} -- $cmd_args &>> "${log_dir}/${name}.log"; then
    fail "could not start ${name}, check ${log_dir}/${name}.log"
  fi
}

# create_node_network creates a network namespace and connects it to the Linux
# bridge using a veth pair
create_node_network() {
  local name="$1"
  local ip="$2"

  # create the namespace
  ip netns add "${name}"

  # create the veth pair
  local veth0="veth${name}0"
  local veth1="veth${name}1"
  ip link add name "${veth0}" type veth peer name "${veth1}"

  # add one end to the bridge
  ip link set dev "${veth0}" master "${BRIDGE_NAME}"
  ip link set dev "${veth0}" up

  # add the other end to the namespace, rename it eth0 and give it the ip
  ip link set dev "${veth1}" netns "${name}"
  ip netns exec "${name}" ip link set dev "${veth1}" name "eth0"
  ip netns exec "${name}" ip link set dev "eth0" up
  ip netns exec "${name}" ip address add "${ip}/24" dev "eth0"
}

create_account() {
  local dir=$1
  local password=$2

  energi3 --datadir "${dir}" --password /dev/stdin account new <<< "${password}"
}

main "$@"
