Browse Source

add terraforce deployment method

pull/1943/head
Ethan Buchman 8 years ago
parent
commit
f53fb46302
18 changed files with 554 additions and 0 deletions
  1. +149
    -0
      terraforce/README.md
  2. +77
    -0
      terraforce/cluster/main.tf
  3. +2
    -0
      terraforce/examples/dummy/bins
  4. +8
    -0
      terraforce/examples/dummy/run.sh
  5. +1
    -0
      terraforce/examples/in-proc-linux/bins
  6. +7
    -0
      terraforce/examples/in-proc-linux/run.sh
  7. +1
    -0
      terraforce/examples/in-proc/bins
  8. +7
    -0
      terraforce/examples/in-proc/run.sh
  9. +30
    -0
      terraforce/main.tf
  10. +10
    -0
      terraforce/scripts/copy_run.sh
  11. +43
    -0
      terraforce/scripts/init.sh
  12. +11
    -0
      terraforce/scripts/query.sh
  13. +10
    -0
      terraforce/scripts/reset.sh
  14. +9
    -0
      terraforce/scripts/restart.sh
  15. +10
    -0
      terraforce/scripts/start.sh
  16. +9
    -0
      terraforce/scripts/stop.sh
  17. +30
    -0
      terraforce/test.sh
  18. +140
    -0
      terraforce/transact/transact.go

+ 149
- 0
terraforce/README.md View File

@ -0,0 +1,149 @@
# Stack
This is a stripped down version of https://github.com/segmentio/stack
plus some shell scripts.
It is responsible for the following:
- spin up a cluster of nodes
- copy config files for a tendermint testnet to each node
- copy linux binaries for tendermint and the app to each node
- start tendermint on every node
# How it Works
To use, a user must only provide a directory containing two files: `bins` and `run.sh`.
The `bins` file is a list of binaries, for instance:
```
$GOPATH/bin/tendermint
$GOPATH/bin/dummy
```
and the `run.sh` specifies how those binaries ought to be started:
```
#! /bin/bash
if [[ "$SEEDS" != "" ]]; then
SEEDS_FLAG="--seeds=$SEEDS"
fi
./dummy --persist .tendermint/data/dummy_data >> app.log 2>&1 &
./tendermint node --log_level=info $SEEDS_FLAG >> tendermint.log 2>&1 &
```
This let's you specify exactly which versions of Tendermint and the application are to be used,
and how they ought to be started.
Note that these binaries *MUST* be compiled for Linux.
If you are not on Linux, you can compile binaries for linux using `go build` with the `GOOS` variable:
```
GOOS=linux go build -o $GOPATH/bin/tendermint-linux $GOPATH/src/github.com/tendermint/tendermint/cmd/tendermint
```
This cross-compilation must be done for each binary you want to copy over.
If you want to use an application that requires more than just a few binaries, you may need to do more manual work,
for instance using `terraforce` to set up the development environment on every machine.
# Dependencies
We use `terraform` for spinning up the machines,
and a custom rolled tool, `terraforce`,
for running commands on many machines in parallel.
You can download terraform here: https://www.terraform.io/downloads.html
To download terraforce, run `go get github.com/ebuchman/terraforce`
We use `tendermint` itself to generate files for a testnet.
You can install `tendermint` with
```
cd $GOPATH/src/github.com/tendermint/tendermint
glide install
go install ./cmd/tendermint
```
You also need to set the `DIGITALOCEAN_TOKEN` environment variables so that terraform can
spin up nodes on digital ocean.
This stack is currently some terraform and a bunch of shell scripts,
so its helpful to work out of a directory containing everything.
Either change directory to `$GOPATH/src/github.com/tendermint/tendermint/test/net`
or make a copy of that directory and change to it. All commands are expected to be executed from there.
For terraform to work, you must first run `terraform get`
# Create
To create a cluster with 4 nodes, run
```
terraform apply
```
To use a different number of nodes, change the `desired_capacity` parameter in the `main.tf`.
Note that terraform keeps track of the current state of your infrastructure,
so if you change the `desired_capacity` and run `terraform apply` again, it will add or remove nodes as necessary.
If you think that's amazing, so do we.
To get some info about the cluster, run `terraform output`.
See the [terraform docs](https://www.terraform.io/docs/index.html) for more details.
To tear down the cluster, run `terraform destroy`.
# Initialize
Now that we have a cluster up and running, let's generate the necessary files for a Tendermint node and copy them over.
A Tendermint node needs, at the least, a `priv_validator.json` and a `genesis.json`.
To generate files for the nodes, run
```
tendermint testnet 4 mytestnet
```
This will create the directory `mytestnet`, containing one directory for each of the 4 nodes.
Each node directory contains a unique `priv_validator.json` and a `genesis.json`,
where the `genesis.json` contains the public keys of all `priv_validator.json` files.
If you want to add more files to each node for your particular app, you'll have to add them to each of the node directories.
Now we can copy everything over to the cluster.
If you are on Linux, run
```
bash scripts/init.sh 4 mytestnet examples/in-proc
```
Otherwise (if you are not on Linux), make sure you ran
```
GOOS=linux go build -o $GOPATH/bin/tendermint-linux $GOPATH/src/github.com/tendermint/tendermint/cmd/tendermint
```
and now run
```
bash scripts/init.sh 4 mytestnet examples/in-proc-linux
```
# Start
Finally, to start Tendermint on all the nodes, run
```
bash scripts/start.sh 4
```
# Check
Query the status of all your nodes:
```
bash scripts/query.sh 4 status
```

+ 77
- 0
terraforce/cluster/main.tf View File

@ -0,0 +1,77 @@
/**
* Cluster on DO
*
*/
variable "name" {
description = "The cluster name, e.g cdn"
}
variable "environment" {
description = "Environment tag, e.g prod"
}
variable "image_id" {
description = "Image ID"
}
variable "regions" {
description = "Regions to launch in"
type = "list"
}
variable "key_ids" {
description = "SSH keys to use"
type = "list"
}
variable "instance_size" {
description = "The instance size to use, e.g 2gb"
}
variable "desired_capacity" {
description = "Desired instance count"
default = 3
}
#-----------------------
# Instances
resource "digitalocean_droplet" "cluster" {
# set the image and instance type
name = "${var.name}${count.index}"
image = "${var.image_id}"
size = "${var.instance_size}"
# the `element` function handles modulo
region = "${element(var.regions, count.index)}"
ssh_keys = "${var.key_ids}"
count = "${var.desired_capacity}"
lifecycle = {
prevent_destroy = false
}
}
#-----------------------
// The cluster name, e.g cdn
output "name" {
value = "${var.name}"
}
// The list of cluster instance ids
output "instances" {
value = ["${digitalocean_droplet.cluster.*.id}"]
}
// The list of cluster instance ips
output "private_ips" {
value = ["${digitalocean_droplet.cluster.*.ipv4_address_private}"]
}
// The list of cluster instance ips
output "public_ips" {
value = ["${digitalocean_droplet.cluster.*.ipv4_address}"]
}

+ 2
- 0
terraforce/examples/dummy/bins View File

@ -0,0 +1,2 @@
$GOPATH/bin/tendermint
$GOPATH/bin/dummy

+ 8
- 0
terraforce/examples/dummy/run.sh View File

@ -0,0 +1,8 @@
#! /bin/bash
if [[ "$SEEDS" != "" ]]; then
SEEDS_FLAG="--seeds=$SEEDS"
fi
./dummy --persist .tendermint/data/dummy_data >> app.log 2>&1 &
./tendermint node --log_level=info $SEEDS_FLAG >> tendermint.log 2>&1 &

+ 1
- 0
terraforce/examples/in-proc-linux/bins View File

@ -0,0 +1 @@
$GOPATH/bin/tendermint-linux

+ 7
- 0
terraforce/examples/in-proc-linux/run.sh View File

@ -0,0 +1,7 @@
#! /bin/bash
if [[ "$SEEDS" != "" ]]; then
SEEDS_FLAG="--seeds=$SEEDS"
fi
./tendermint-linux node --proxy_app=dummy --log_level=note $SEEDS_FLAG >> tendermint.log 2>&1 &

+ 1
- 0
terraforce/examples/in-proc/bins View File

@ -0,0 +1 @@
$GOPATH/bin/tendermint

+ 7
- 0
terraforce/examples/in-proc/run.sh View File

@ -0,0 +1,7 @@
#! /bin/bash
if [[ "$SEEDS" != "" ]]; then
SEEDS_FLAG="--seeds=$SEEDS"
fi
./tendermint node --proxy_app=dummy --log_level=note $SEEDS_FLAG >> tendermint.log 2>&1 &

+ 30
- 0
terraforce/main.tf View File

@ -0,0 +1,30 @@
module "cluster" {
source = "./cluster"
environment = "test"
name = "tendermint-testnet"
# curl -X GET -H "Content-Type: application/json" -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" "https://api.digitalocean.com/v2/account/keys"
key_ids = [8163311]
image_id = "ubuntu-14-04-x64"
desired_capacity = 4
instance_size = "2gb"
regions = ["AMS2", "FRA1", "LON1", "NYC2", "SFO2", "SGP1", "TOR1"]
}
provider "digitalocean" {
}
output "public_ips" {
value = "${module.cluster.public_ips}"
}
output "private_ips" {
value = "${join(",",module.cluster.private_ips)}"
}
output "seeds" {
value = "${join(":46656,",module.cluster.public_ips)}:46656"
}

+ 10
- 0
terraforce/scripts/copy_run.sh View File

@ -0,0 +1,10 @@
#! /bin/bash
set -u
N=$1 # number of nodes
RUN=$2 # path to run script
N_=$((N-1))
# stop all tendermint
terraforce scp --user root --ssh-key $HOME/.ssh/id_rsa --machines "[0-$N_]" $RUN run.sh

+ 43
- 0
terraforce/scripts/init.sh View File

@ -0,0 +1,43 @@
#! /bin/bash
set -u
N=$1 # number of nodes
TESTNET=$2 # path to folder containing testnet info
CONFIG=$3 # path to folder containing `bins` and `run.sh` files
if [[ ! -f $CONFIG/bins ]]; then
echo "config folder ($CONFIG) must contain bins file"
exit 1
fi
if [[ ! -f $CONFIG/run.sh ]]; then
echo "config folder ($CONFIG) must contain run.sh file"
exit 1
fi
KEY=$HOME/.ssh/id_rsa
FLAGS="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
N_=$((N-1)) # 0-based index
MACH_ROOT="$TESTNET/mach?"
# mkdir
terraforce ssh --user root --ssh-key $KEY --machines "[0-$N_]" mkdir .tendermint
# copy over genesis/priv_val
terraforce scp --user root --ssh-key $KEY --iterative --machines "[0-$N_]" "$MACH_ROOT/priv_validator.json" .tendermint/priv_validator.json
terraforce scp --user root --ssh-key $KEY --iterative --machines "[0-$N_]" "$MACH_ROOT/genesis.json" .tendermint/genesis.json
# copy the run script
terraforce scp --user root --ssh-key $KEY --machines "[0-$N_]" $CONFIG/run.sh run.sh
# copy the binaries
while read line; do
local_bin=$(eval echo $line)
remote_bin=$(basename $local_bin)
echo $local_bin
terraforce scp --user root --ssh-key $KEY --machines "[0-$N_]" $local_bin $remote_bin
terraforce ssh --user root --ssh-key $KEY --machines "[0-$N_]" chmod +x $remote_bin
done <$CONFIG/bins

+ 11
- 0
terraforce/scripts/query.sh View File

@ -0,0 +1,11 @@
#! /bin/bash
set -u
N=$1 # number of nodes
QUERY=$2
N_=$((N-1))
# start all tendermint nodes
terraforce ssh --user root --ssh-key $HOME/.ssh/id_rsa --machines "[0-$N_]" curl -s localhost:46657/$QUERY

+ 10
- 0
terraforce/scripts/reset.sh View File

@ -0,0 +1,10 @@
#! /bin/bash
set -u
N=$1 # number of nodes
N_=$((N-1))
# stop all tendermint
terraforce ssh --user root --ssh-key $HOME/.ssh/id_rsa --machines "[0-$N_]" rm -rf .tendermint/data
terraforce ssh --user root --ssh-key $HOME/.ssh/id_rsa --machines "[0-$N_]" ./tendermint unsafe_reset_priv_validator

+ 9
- 0
terraforce/scripts/restart.sh View File

@ -0,0 +1,9 @@
#! /bin/bash
set -u
N=$1 # number of nodes
N_=$((N-1))
# start
terraforce ssh --user root --ssh-key $HOME/.ssh/id_rsa --machines "[0-$N_]" SEEDS=$(terraform output seeds) bash run.sh

+ 10
- 0
terraforce/scripts/start.sh View File

@ -0,0 +1,10 @@
#! /bin/bash
set -u
N=$1 # number of nodes
N_=$((N-1))
# start all tendermint nodes
terraforce ssh --user root --ssh-key $HOME/.ssh/id_rsa --machines "[0-$N_]" SEEDS=$(terraform output seeds) bash run.sh

+ 9
- 0
terraforce/scripts/stop.sh View File

@ -0,0 +1,9 @@
#! /bin/bash
set -u
N=$1 # number of nodes
N_=$((N-1))
# stop all tendermint
terraforce ssh --user root --ssh-key $HOME/.ssh/id_rsa --machines "[0-$N_]" killall tendermint

+ 30
- 0
terraforce/test.sh View File

@ -0,0 +1,30 @@
#! /bin/bash
cd $GOPATH/src/github.com/tendermint/tendermint
TEST_PATH=./test/net/new
N=4
TESTNET_DIR=mytestnet
# install deps
# TODO: we should build a Docker image and
# really do everything that follows in the container
# bash setup.sh
# launch infra
terraform get
terraform apply
# create testnet files
tendermint testnet -n $N -dir $TESTNET_DIR
# expects a linux tendermint binary to be built already
bash scripts/init.sh $N $TESTNET_DIR test/net/examples/in-proc
# testnet should now be running :)
bash scripts/start.sh 4

+ 140
- 0
terraforce/transact/transact.go View File

@ -0,0 +1,140 @@
package main
import (
"crypto/rand"
"encoding/binary"
"encoding/hex"
"flag"
"fmt"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/tendermint/go-rpc/client"
rpctypes "github.com/tendermint/go-rpc/types"
)
func main() {
flag.Parse()
args := flag.Args()
if len(args) < 2 {
fmt.Println("transact.go expects at least two arguments (ntxs, hosts)")
os.Exit(1)
}
nTxS, hostS := args[0], args[1]
nTxs, err := strconv.Atoi(nTxS)
if err != nil {
fmt.Println("ntxs must be an integer:", err)
os.Exit(1)
}
hosts := strings.Split(hostS, ",")
errCh := make(chan error, 1000)
wg := new(sync.WaitGroup)
wg.Add(len(hosts))
start := time.Now()
fmt.Printf("Sending %d txs on every host %v\n", nTxs, hosts)
for i, host := range hosts {
go broadcastTxsToHost(wg, errCh, i, host, nTxs, 0)
}
wg.Wait()
fmt.Println("Done broadcasting txs. Took", time.Since(start))
}
func broadcastTxsToHost(wg *sync.WaitGroup, errCh chan error, valI int, valHost string, nTxs int, txCount int) {
reconnectSleepSeconds := time.Second * 1
// thisStart := time.Now()
// cli := rpcclient.NewClientURI(valHost + ":46657")
fmt.Println("Connecting to host to broadcast txs", valI, valHost)
cli := rpcclient.NewWSClient(valHost, "/websocket")
if _, err := cli.Start(); err != nil {
if nTxs == 0 {
time.Sleep(reconnectSleepSeconds)
broadcastTxsToHost(wg, errCh, valI, valHost, nTxs, txCount)
return
}
fmt.Printf("Error starting websocket connection to val%d (%s): %v\n", valI, valHost, err)
os.Exit(1)
}
reconnect := make(chan struct{})
go func(count int) {
LOOP:
for {
ticker := time.NewTicker(reconnectSleepSeconds)
select {
case <-cli.ResultsCh:
count += 1
// nTxs == 0 means just loop forever
if nTxs > 0 && count == nTxs {
break LOOP
}
case err := <-cli.ErrorsCh:
fmt.Println("err: val", valI, valHost, err)
case <-cli.Quit:
broadcastTxsToHost(wg, errCh, valI, valHost, nTxs, count)
return
case <-reconnect:
broadcastTxsToHost(wg, errCh, valI, valHost, nTxs, count)
return
case <-ticker.C:
if nTxs == 0 {
cli.Stop()
broadcastTxsToHost(wg, errCh, valI, valHost, nTxs, count)
return
}
}
}
fmt.Printf("Received all responses from node %d (%s)\n", valI, valHost)
wg.Done()
}(txCount)
var i = 0
for {
/* if i%(nTxs/4) == 0 {
fmt.Printf("Have sent %d txs to node %d. Total time so far: %v\n", i, valI, time.Since(thisStart))
}*/
if !cli.IsRunning() {
return
}
tx := generateTx(i, valI)
if err := cli.WriteJSON(rpctypes.RPCRequest{
JSONRPC: "2.0",
ID: "",
Method: "broadcast_tx_async",
Params: []interface{}{hex.EncodeToString(tx)},
}); err != nil {
fmt.Printf("Error sending tx %d to validator %d: %v. Attempt reconnect\n", i, valI, err)
reconnect <- struct{}{}
return
}
i += 1
if nTxs > 0 && i >= nTxs {
break
} else if nTxs == 0 {
time.Sleep(time.Millisecond * 1)
}
}
fmt.Printf("Done sending %d txs to node s%d (%s)\n", nTxs, valI, valHost)
}
func generateTx(i, valI int) []byte {
// a tx encodes the validator index, the tx number, and some random junk
// TODO: read random bytes into more of the tx
tx := make([]byte, 250)
binary.PutUvarint(tx[:32], uint64(valI))
binary.PutUvarint(tx[32:64], uint64(i))
if _, err := rand.Read(tx[234:]); err != nil {
fmt.Println("err reading from crypto/rand", err)
os.Exit(1)
}
return tx
}

Loading…
Cancel
Save