Преглед на файлове

swarm/dev: add development environment (#14332)

This PR adds a Swarm development environment which can be run in a
Docker container and provides scripts for building binaries and running
Swarm clusters.
Lewis Marshall преди 8 години
родител
ревизия
0036e2a747

+ 2 - 0
swarm/dev/.dockerignore

@@ -0,0 +1,2 @@
+bin/*
+cluster/*

+ 2 - 0
swarm/dev/.gitignore

@@ -0,0 +1,2 @@
+bin/*
+cluster/*

+ 42 - 0
swarm/dev/Dockerfile

@@ -0,0 +1,42 @@
+FROM ubuntu:xenial
+
+# install build + test dependencies
+RUN apt-get update && \
+    apt-get install --yes --no-install-recommends \
+      ca-certificates \
+      curl \
+      fuse \
+      g++ \
+      gcc \
+      git \
+      iproute2 \
+      iputils-ping \
+      less \
+      libc6-dev \
+      make \
+      pkg-config \
+      && \
+    apt-get clean
+
+# install Go
+ENV GO_VERSION 1.8.1
+RUN curl -fSLo golang.tar.gz "https://golang.org/dl/go${GO_VERSION}.linux-amd64.tar.gz" && \
+    tar -xzf golang.tar.gz -C /usr/local && \
+    rm golang.tar.gz
+ENV GOPATH /go
+ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
+
+# install docker CLI
+RUN curl -fSLo docker.tar.gz https://get.docker.com/builds/Linux/x86_64/docker-17.04.0-ce.tgz && \
+  tar -xzf docker.tar.gz -C /usr/local/bin --strip-components=1 docker/docker && \
+  rm docker.tar.gz
+
+# install jq
+RUN curl -fSLo /usr/local/bin/jq https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64 && \
+    chmod +x /usr/local/bin/jq
+
+# install govendor
+RUN go get -u github.com/kardianos/govendor
+
+# add custom bashrc
+ADD bashrc /root/.bashrc

+ 14 - 0
swarm/dev/Makefile

@@ -0,0 +1,14 @@
+.PHONY: build cluster test
+
+default: build
+
+build:
+	go build -o bin/swarm    github.com/ethereum/go-ethereum/cmd/swarm
+	go build -o bin/geth     github.com/ethereum/go-ethereum/cmd/geth
+	go build -o bin/bootnode github.com/ethereum/go-ethereum/cmd/bootnode
+
+cluster: build
+	scripts/boot-cluster.sh
+
+test:
+	go test -v github.com/ethereum/go-ethereum/swarm/...

+ 20 - 0
swarm/dev/README.md

@@ -0,0 +1,20 @@
+Swarm development environment
+=============================
+
+The Swarm development environment is a Linux bash shell which can be run in a
+Docker container and provides a predictable build and test environment.
+
+### Start the Docker container
+
+Run the `run.sh` script to build the Docker image and run it, you will then be
+at a bash prompt inside the `swarm/dev` directory.
+
+### Build binaries
+
+Run `make` to build the `swarm`, `geth` and `bootnode` binaries into the
+`swarm/dev/bin` directory.
+
+### Boot a cluster
+
+Run `make cluster` to start a 3 node Swarm cluster, or run
+`scripts/boot-cluster.sh --size N` to boot a cluster of size N.

+ 21 - 0
swarm/dev/bashrc

@@ -0,0 +1,21 @@
+export ROOT="${GOPATH}/src/github.com/ethereum/go-ethereum"
+export PATH="${ROOT}/swarm/dev/bin:${PATH}"
+
+cd "${ROOT}/swarm/dev"
+
+cat <<WELCOME
+
+=============================================
+
+Welcome to the swarm development environment.
+
+- Run 'make' to build the swarm, geth and bootnode binaries
+- Run 'make test' to run the swarm unit tests
+- Run 'make cluster' to start a swarm cluster
+- Run 'exit' to exit the development environment
+
+See the 'scripts' directory for some useful scripts.
+
+=============================================
+
+WELCOME

+ 90 - 0
swarm/dev/run.sh

@@ -0,0 +1,90 @@
+#!/usr/bin/env bash
+#
+# A script to build and run the Swarm development environment using Docker.
+
+set -e
+
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+
+# DEFAULT_NAME is the default name for the Docker image and container
+DEFAULT_NAME="swarm-dev"
+
+usage() {
+  cat >&2 <<USAGE
+usage: $0 [options]
+
+Build and run the Swarm development environment.
+
+Depends on Docker being installed locally.
+
+OPTIONS:
+  -n, --name NAME          Docker image and container name [default: ${DEFAULT_NAME}]
+  -d, --docker-args ARGS   Custom args to pass to 'docker run' (e.g. '-p 8000:8000' to expose a port)
+  -h, --help               Show this message
+USAGE
+}
+
+main() {
+  local name="${DEFAULT_NAME}"
+  local docker_args=""
+  parse_args "$@"
+  build_image
+  run_image
+}
+
+parse_args() {
+  while true; do
+    case "$1" in
+      -h | --help)
+        usage
+        exit 0
+        ;;
+      -n | --name)
+        if [[ -z "$2" ]]; then
+          echo "ERROR: --name flag requires an argument" >&2
+          exit 1
+        fi
+        name="$2"
+        shift 2
+        ;;
+      -d | --docker-args)
+        if [[ -z "$2" ]]; then
+          echo "ERROR: --docker-args flag requires an argument" >&2
+          exit 1
+        fi
+        docker_args="$2"
+        shift 2
+        ;;
+      *)
+        break
+        ;;
+    esac
+  done
+
+  if [[ $# -ne 0 ]]; then
+    usage
+    echo "ERROR: invalid arguments" >&2
+    exit 1
+  fi
+}
+
+build_image() {
+  docker build --tag "${name}" "${ROOT}/swarm/dev"
+}
+
+run_image() {
+  exec docker run \
+    --privileged \
+    --interactive \
+    --tty \
+    --rm \
+    --hostname "${name}" \
+    --name     "${name}" \
+    --volume   "${ROOT}:/go/src/github.com/ethereum/go-ethereum" \
+    --volume   "/var/run/docker.sock:/var/run/docker.sock" \
+    ${docker_args} \
+    "${name}" \
+    /bin/bash
+}
+
+main "$@"

+ 288 - 0
swarm/dev/scripts/boot-cluster.sh

@@ -0,0 +1,288 @@
+#!/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 geth 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
+# geth:     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 geth configuration
+GETH_IP="192.168.33.3"
+GETH_RPC_PORT="8545"
+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_geth_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 geth 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_geth_node starts a geth node with --datadir pointing at <base-dir>/geth
+# and a single, unlocked account with password "geth"
+start_geth_node() {
+  local dir="${base_dir}/geth"
+  mkdir -p "${dir}"
+
+  local password="geth"
+  echo "${password}" > "${dir}/password"
+
+  # create an account if necessary
+  if [[ ! -e "${dir}/keystore" ]]; then
+    info "creating geth 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 geth 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 "geth" "${GETH_IP}" "$(which geth)" ${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}"
+    --ethapi       "${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
+
+  geth --datadir "${dir}" --password /dev/stdin account new <<< "${password}"
+}
+
+main "$@"

+ 96 - 0
swarm/dev/scripts/random-uploads.sh

@@ -0,0 +1,96 @@
+#!/bin/bash
+#
+# A script to upload random data to a swarm cluster.
+#
+# Example:
+#
+#   random-uploads.sh --addr 192.168.33.101:8500 --size 40k --count 1000
+
+set -e
+
+ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
+source "${ROOT}/swarm/dev/scripts/util.sh"
+
+DEFAULT_ADDR="localhost:8500"
+DEFAULT_UPLOAD_SIZE="40k"
+DEFAULT_UPLOAD_COUNT="1000"
+
+usage() {
+  cat >&2 <<USAGE
+usage: $0 [options]
+
+Upload random data to a Swarm cluster.
+
+OPTIONS:
+  -a, --addr ADDR     Swarm API address      [default: ${DEFAULT_ADDR}]
+  -s, --size SIZE     Individual upload size [default: ${DEFAULT_UPLOAD_SIZE}]
+  -c, --count COUNT   Number of uploads      [default: ${DEFAULT_UPLOAD_COUNT}]
+  -h, --help          Show this message
+USAGE
+}
+
+main() {
+  local addr="${DEFAULT_ADDR}"
+  local upload_size="${DEFAULT_UPLOAD_SIZE}"
+  local upload_count="${DEFAULT_UPLOAD_COUNT}"
+
+  parse_args "$@"
+
+  info "uploading ${upload_count} ${upload_size} random files to ${addr}"
+
+  for i in $(seq 1 ${upload_count}); do
+    info "upload ${i} / ${upload_count}:"
+    do_random_upload
+    echo
+  done
+}
+
+do_random_upload() {
+  curl -fsSL -X POST --data-binary "$(random_data)" "http://${addr}/bzzr:/"
+}
+
+random_data() {
+  dd if=/dev/urandom of=/dev/stdout bs="${upload_size}" count=1 2>/dev/null
+}
+
+parse_args() {
+  while true; do
+    case "$1" in
+      -h | --help)
+        usage
+        exit 0
+        ;;
+      -a | --addr)
+        if [[ -z "$2" ]]; then
+          fail "--addr flag requires an argument"
+        fi
+        addr="$2"
+        shift 2
+        ;;
+      -s | --size)
+        if [[ -z "$2" ]]; then
+          fail "--size flag requires an argument"
+        fi
+        upload_size="$2"
+        shift 2
+        ;;
+      -c | --count)
+        if [[ -z "$2" ]]; then
+          fail "--count flag requires an argument"
+        fi
+        upload_count="$2"
+        shift 2
+        ;;
+      *)
+        break
+        ;;
+    esac
+  done
+
+  if [[ $# -ne 0 ]]; then
+    usage
+    fail "ERROR: invalid arguments: $@"
+  fi
+}
+
+main "$@"

+ 98 - 0
swarm/dev/scripts/stop-cluster.sh

@@ -0,0 +1,98 @@
+#!/bin/bash
+#
+# A script to shutdown a dev swarm cluster.
+
+set -e
+
+ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
+source "${ROOT}/swarm/dev/scripts/util.sh"
+
+DEFAULT_BASE_DIR="${ROOT}/swarm/dev/cluster"
+
+usage() {
+  cat >&2 <<USAGE
+usage: $0 [options]
+
+Shutdown a dev swarm cluster.
+
+OPTIONS:
+  -d, --dir DIR     Base directory [default: ${DEFAULT_BASE_DIR}]
+  -h, --help        Show this message
+USAGE
+}
+
+main() {
+  local base_dir="${DEFAULT_BASE_DIR}"
+
+  parse_args "$@"
+
+  local pid_dir="${base_dir}/pids"
+
+  stop_swarm_nodes
+  stop_node "geth"
+  stop_node "bootnode"
+  delete_network
+}
+
+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
+        ;;
+      *)
+        break
+        ;;
+    esac
+  done
+
+  if [[ $# -ne 0 ]]; then
+    usage
+    fail "ERROR: invalid arguments: $@"
+  fi
+}
+
+stop_swarm_nodes() {
+  for name in $(ls "${pid_dir}" | grep -oP 'swarm\d+'); do
+    stop_node "${name}"
+  done
+}
+
+stop_node() {
+  local name=$1
+  local pid_file="${pid_dir}/${name}.pid"
+
+  if [[ -e "${pid_file}" ]]; then
+    info "stopping ${name}"
+    start-stop-daemon \
+      --stop \
+      --pidfile "${pid_file}" \
+      --remove-pidfile \
+      --oknodo \
+      --retry 15
+  fi
+
+  if ip netns list | grep -qF "${name}"; then
+    ip netns delete "${name}"
+  fi
+
+  if ip link show "veth${name}0" &>/dev/null; then
+    ip link delete dev "veth${name}0"
+  fi
+}
+
+delete_network() {
+  if ip link show "swarmbr0" &>/dev/null; then
+    ip link delete dev "swarmbr0"
+  fi
+}
+
+main "$@"

+ 53 - 0
swarm/dev/scripts/util.sh

@@ -0,0 +1,53 @@
+# shared shell functions
+
+info() {
+  local msg="$@"
+  local timestamp="$(date +%H:%M:%S)"
+  say "===> ${timestamp} ${msg}" "green"
+}
+
+warn() {
+  local msg="$@"
+  local timestamp=$(date +%H:%M:%S)
+  say "===> ${timestamp} WARN: ${msg}" "yellow" >&2
+}
+
+fail() {
+  local msg="$@"
+  say "ERROR: ${msg}" "red" >&2
+  exit 1
+}
+
+# say prints the given message to STDOUT, using the optional color if
+# STDOUT is a terminal.
+#
+# usage:
+#
+#   say "foo"              - prints "foo"
+#   say "bar" "red"        - prints "bar" in red
+#   say "baz" "green"      - prints "baz" in green
+#   say "qux" "red" | tee  - prints "qux" with no colour
+#
+say() {
+  local msg=$1
+  local color=$2
+
+  if [[ -n "${color}" ]] && [[ -t 1 ]]; then
+    case "${color}" in
+      red)
+        echo -e "\033[1;31m${msg}\033[0m"
+        ;;
+      green)
+        echo -e "\033[1;32m${msg}\033[0m"
+        ;;
+      yellow)
+        echo -e "\033[1;33m${msg}\033[0m"
+        ;;
+      *)
+        echo "${msg}"
+        ;;
+    esac
+  else
+    echo "${msg}"
+  fi
+}