From 0036e2a74761413200ce3a8ed316ecb721895f60 Mon Sep 17 00:00:00 2001 From: Lewis Marshall Date: Thu, 1 Jun 2017 11:52:18 +0100 Subject: [PATCH] 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. --- swarm/dev/.dockerignore | 2 + swarm/dev/.gitignore | 2 + swarm/dev/Dockerfile | 42 ++++ swarm/dev/Makefile | 14 ++ swarm/dev/README.md | 20 ++ swarm/dev/bashrc | 21 ++ swarm/dev/run.sh | 90 +++++++++ swarm/dev/scripts/boot-cluster.sh | 288 ++++++++++++++++++++++++++++ swarm/dev/scripts/random-uploads.sh | 96 ++++++++++ swarm/dev/scripts/stop-cluster.sh | 98 ++++++++++ swarm/dev/scripts/util.sh | 53 +++++ 11 files changed, 726 insertions(+) create mode 100644 swarm/dev/.dockerignore create mode 100644 swarm/dev/.gitignore create mode 100644 swarm/dev/Dockerfile create mode 100644 swarm/dev/Makefile create mode 100644 swarm/dev/README.md create mode 100644 swarm/dev/bashrc create mode 100755 swarm/dev/run.sh create mode 100755 swarm/dev/scripts/boot-cluster.sh create mode 100755 swarm/dev/scripts/random-uploads.sh create mode 100755 swarm/dev/scripts/stop-cluster.sh create mode 100644 swarm/dev/scripts/util.sh diff --git a/swarm/dev/.dockerignore b/swarm/dev/.dockerignore new file mode 100644 index 0000000000..f9e69b37f3 --- /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 0000000000..f9e69b37f3 --- /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 0000000000..728bdab1fb --- /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 0000000000..365964b7f5 --- /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 0000000000..81e3b53585 --- /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 0000000000..efb504fa36 --- /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 <&2 <&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 0000000000..073b095ad4 --- /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 < "${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 /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 / +# (e.g. /swarm07) and a single account with 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" <>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +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 0000000000..db7887e3c0 --- /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 </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 0000000000..89cb7b0c9a --- /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 </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 0000000000..f17a12e420 --- /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 +}