diff options
-rw-r--r-- | swarm/dev/.dockerignore | 2 | ||||
-rw-r--r-- | swarm/dev/.gitignore | 2 | ||||
-rw-r--r-- | swarm/dev/Dockerfile | 42 | ||||
-rw-r--r-- | swarm/dev/Makefile | 14 | ||||
-rw-r--r-- | swarm/dev/README.md | 20 | ||||
-rw-r--r-- | swarm/dev/bashrc | 21 | ||||
-rwxr-xr-x | swarm/dev/run.sh | 90 | ||||
-rwxr-xr-x | swarm/dev/scripts/boot-cluster.sh | 288 | ||||
-rwxr-xr-x | swarm/dev/scripts/random-uploads.sh | 96 | ||||
-rwxr-xr-x | swarm/dev/scripts/stop-cluster.sh | 98 | ||||
-rw-r--r-- | swarm/dev/scripts/util.sh | 53 |
11 files changed, 726 insertions, 0 deletions
diff --git a/swarm/dev/.dockerignore b/swarm/dev/.dockerignore new file mode 100644 index 000000000..f9e69b37f --- /dev/null +++ b/swarm/dev/.dockerignore @@ -0,0 +1,2 @@ +bin/* +cluster/* diff --git a/swarm/dev/.gitignore b/swarm/dev/.gitignore new file mode 100644 index 000000000..f9e69b37f --- /dev/null +++ b/swarm/dev/.gitignore @@ -0,0 +1,2 @@ +bin/* +cluster/* diff --git a/swarm/dev/Dockerfile b/swarm/dev/Dockerfile new file mode 100644 index 000000000..728bdab1f --- /dev/null +++ b/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 diff --git a/swarm/dev/Makefile b/swarm/dev/Makefile new file mode 100644 index 000000000..365964b7f --- /dev/null +++ b/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/... diff --git a/swarm/dev/README.md b/swarm/dev/README.md new file mode 100644 index 000000000..81e3b5358 --- /dev/null +++ b/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. diff --git a/swarm/dev/bashrc b/swarm/dev/bashrc new file mode 100644 index 000000000..efb504fa3 --- /dev/null +++ b/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 diff --git a/swarm/dev/run.sh b/swarm/dev/run.sh new file mode 100755 index 000000000..2ad600ded --- /dev/null +++ b/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 "$@" diff --git a/swarm/dev/scripts/boot-cluster.sh b/swarm/dev/scripts/boot-cluster.sh new file mode 100755 index 000000000..073b095ad --- /dev/null +++ b/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 "$@" diff --git a/swarm/dev/scripts/random-uploads.sh b/swarm/dev/scripts/random-uploads.sh new file mode 100755 index 000000000..db7887e3c --- /dev/null +++ b/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 "$@" diff --git a/swarm/dev/scripts/stop-cluster.sh b/swarm/dev/scripts/stop-cluster.sh new file mode 100755 index 000000000..89cb7b0c9 --- /dev/null +++ b/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 "$@" diff --git a/swarm/dev/scripts/util.sh b/swarm/dev/scripts/util.sh new file mode 100644 index 000000000..f17a12e42 --- /dev/null +++ b/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 +} |