2017-04-10 23:25:53 +00:00
|
|
|
// Copyright 2017 The go-ethereum Authors
|
|
|
|
// This file is part of go-ethereum.
|
|
|
|
//
|
|
|
|
// go-ethereum is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// go-ethereum is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
|
|
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
|
|
|
"math/rand"
|
|
|
|
"path/filepath"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"text/template"
|
|
|
|
|
|
|
|
"github.com/ethereum/go-ethereum/log"
|
|
|
|
)
|
|
|
|
|
|
|
|
// nodeDockerfile is the Dockerfile required to run an Ethereum node.
|
|
|
|
var nodeDockerfile = `
|
2017-09-05 13:06:36 +00:00
|
|
|
FROM ethereum/client-go:latest
|
2017-04-10 23:25:53 +00:00
|
|
|
|
|
|
|
ADD genesis.json /genesis.json
|
|
|
|
{{if .Unlock}}
|
|
|
|
ADD signer.json /signer.json
|
|
|
|
ADD signer.pass /signer.pass
|
|
|
|
{{end}}
|
|
|
|
RUN \
|
2017-09-05 13:06:36 +00:00
|
|
|
echo 'geth init /genesis.json' > geth.sh && \{{if .Unlock}}
|
2017-04-10 23:25:53 +00:00
|
|
|
echo 'mkdir -p /root/.ethereum/keystore/ && cp /signer.json /root/.ethereum/keystore/' >> geth.sh && \{{end}}
|
2017-09-05 13:06:36 +00:00
|
|
|
echo $'geth --networkid {{.NetworkID}} --cache 512 --port {{.Port}} --maxpeers {{.Peers}} {{.LightFlag}} --ethstats \'{{.Ethstats}}\' {{if .BootV4}}--bootnodesv4 {{.BootV4}}{{end}} {{if .BootV5}}--bootnodesv5 {{.BootV5}}{{end}} {{if .Etherbase}}--etherbase {{.Etherbase}} --mine{{end}}{{if .Unlock}}--unlock 0 --password /signer.pass --mine{{end}} --targetgaslimit {{.GasTarget}} --gasprice {{.GasPrice}}' >> geth.sh
|
2017-04-10 23:25:53 +00:00
|
|
|
|
|
|
|
ENTRYPOINT ["/bin/sh", "geth.sh"]
|
|
|
|
`
|
|
|
|
|
|
|
|
// nodeComposefile is the docker-compose.yml file required to deploy and maintain
|
|
|
|
// an Ethereum node (bootnode or miner for now).
|
|
|
|
var nodeComposefile = `
|
|
|
|
version: '2'
|
|
|
|
services:
|
|
|
|
{{.Type}}:
|
|
|
|
build: .
|
|
|
|
image: {{.Network}}/{{.Type}}
|
|
|
|
ports:
|
|
|
|
- "{{.FullPort}}:{{.FullPort}}"
|
|
|
|
- "{{.FullPort}}:{{.FullPort}}/udp"{{if .Light}}
|
|
|
|
- "{{.LightPort}}:{{.LightPort}}/udp"{{end}}
|
|
|
|
volumes:
|
|
|
|
- {{.Datadir}}:/root/.ethereum
|
|
|
|
environment:
|
|
|
|
- FULL_PORT={{.FullPort}}/tcp
|
|
|
|
- LIGHT_PORT={{.LightPort}}/udp
|
|
|
|
- TOTAL_PEERS={{.TotalPeers}}
|
|
|
|
- LIGHT_PEERS={{.LightPeers}}
|
|
|
|
- STATS_NAME={{.Ethstats}}
|
|
|
|
- MINER_NAME={{.Etherbase}}
|
2017-05-13 00:03:56 +00:00
|
|
|
- GAS_TARGET={{.GasTarget}}
|
|
|
|
- GAS_PRICE={{.GasPrice}}
|
2017-07-17 17:38:40 +00:00
|
|
|
logging:
|
|
|
|
driver: "json-file"
|
|
|
|
options:
|
|
|
|
max-size: "1m"
|
|
|
|
max-file: "10"
|
2017-04-10 23:25:53 +00:00
|
|
|
restart: always
|
|
|
|
`
|
|
|
|
|
|
|
|
// deployNode deploys a new Ethereum node container to a remote machine via SSH,
|
|
|
|
// docker and docker-compose. If an instance with the specified network name
|
|
|
|
// already exists there, it will be overwritten!
|
2017-05-13 00:03:56 +00:00
|
|
|
func deployNode(client *sshClient, network string, bootv4, bootv5 []string, config *nodeInfos) ([]byte, error) {
|
2017-04-10 23:25:53 +00:00
|
|
|
kind := "sealnode"
|
|
|
|
if config.keyJSON == "" && config.etherbase == "" {
|
|
|
|
kind = "bootnode"
|
2017-05-13 00:03:56 +00:00
|
|
|
bootv4 = make([]string, 0)
|
|
|
|
bootv5 = make([]string, 0)
|
2017-04-10 23:25:53 +00:00
|
|
|
}
|
|
|
|
// Generate the content to upload to the server
|
|
|
|
workdir := fmt.Sprintf("%d", rand.Int63())
|
|
|
|
files := make(map[string][]byte)
|
|
|
|
|
|
|
|
lightFlag := ""
|
|
|
|
if config.peersLight > 0 {
|
|
|
|
lightFlag = fmt.Sprintf("--lightpeers=%d --lightserv=50", config.peersLight)
|
|
|
|
}
|
|
|
|
dockerfile := new(bytes.Buffer)
|
|
|
|
template.Must(template.New("").Parse(nodeDockerfile)).Execute(dockerfile, map[string]interface{}{
|
|
|
|
"NetworkID": config.network,
|
|
|
|
"Port": config.portFull,
|
|
|
|
"Peers": config.peersTotal,
|
|
|
|
"LightFlag": lightFlag,
|
2017-05-13 00:03:56 +00:00
|
|
|
"BootV4": strings.Join(bootv4, ","),
|
|
|
|
"BootV5": strings.Join(bootv5, ","),
|
2017-04-10 23:25:53 +00:00
|
|
|
"Ethstats": config.ethstats,
|
|
|
|
"Etherbase": config.etherbase,
|
2017-05-13 00:03:56 +00:00
|
|
|
"GasTarget": uint64(1000000 * config.gasTarget),
|
|
|
|
"GasPrice": uint64(1000000000 * config.gasPrice),
|
2017-04-10 23:25:53 +00:00
|
|
|
"Unlock": config.keyJSON != "",
|
|
|
|
})
|
|
|
|
files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
|
|
|
|
|
|
|
|
composefile := new(bytes.Buffer)
|
|
|
|
template.Must(template.New("").Parse(nodeComposefile)).Execute(composefile, map[string]interface{}{
|
|
|
|
"Type": kind,
|
|
|
|
"Datadir": config.datadir,
|
|
|
|
"Network": network,
|
|
|
|
"FullPort": config.portFull,
|
|
|
|
"TotalPeers": config.peersTotal,
|
|
|
|
"Light": config.peersLight > 0,
|
|
|
|
"LightPort": config.portFull + 1,
|
|
|
|
"LightPeers": config.peersLight,
|
|
|
|
"Ethstats": config.ethstats[:strings.Index(config.ethstats, ":")],
|
|
|
|
"Etherbase": config.etherbase,
|
2017-05-13 00:03:56 +00:00
|
|
|
"GasTarget": config.gasTarget,
|
|
|
|
"GasPrice": config.gasPrice,
|
2017-04-10 23:25:53 +00:00
|
|
|
})
|
|
|
|
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
|
|
|
|
|
|
|
|
//genesisfile, _ := json.MarshalIndent(config.genesis, "", " ")
|
2017-11-10 17:06:45 +00:00
|
|
|
files[filepath.Join(workdir, "genesis.json")] = config.genesis
|
2017-04-10 23:25:53 +00:00
|
|
|
|
|
|
|
if config.keyJSON != "" {
|
|
|
|
files[filepath.Join(workdir, "signer.json")] = []byte(config.keyJSON)
|
|
|
|
files[filepath.Join(workdir, "signer.pass")] = []byte(config.keyPass)
|
|
|
|
}
|
|
|
|
// Upload the deployment files to the remote server (and clean up afterwards)
|
|
|
|
if out, err := client.Upload(files); err != nil {
|
|
|
|
return out, err
|
|
|
|
}
|
|
|
|
defer client.Run("rm -rf " + workdir)
|
|
|
|
|
2017-05-30 11:24:01 +00:00
|
|
|
// Build and deploy the boot or seal node service
|
2017-04-10 23:25:53 +00:00
|
|
|
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build", workdir, network))
|
|
|
|
}
|
|
|
|
|
|
|
|
// nodeInfos is returned from a boot or seal node status check to allow reporting
|
|
|
|
// various configuration parameters.
|
|
|
|
type nodeInfos struct {
|
|
|
|
genesis []byte
|
|
|
|
network int64
|
|
|
|
datadir string
|
|
|
|
ethstats string
|
|
|
|
portFull int
|
|
|
|
portLight int
|
|
|
|
enodeFull string
|
|
|
|
enodeLight string
|
|
|
|
peersTotal int
|
|
|
|
peersLight int
|
|
|
|
etherbase string
|
|
|
|
keyJSON string
|
|
|
|
keyPass string
|
2017-05-13 00:03:56 +00:00
|
|
|
gasTarget float64
|
|
|
|
gasPrice float64
|
2017-04-10 23:25:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// String implements the stringer interface.
|
|
|
|
func (info *nodeInfos) String() string {
|
|
|
|
discv5 := ""
|
|
|
|
if info.peersLight > 0 {
|
|
|
|
discv5 = fmt.Sprintf(", portv5=%d", info.portLight)
|
|
|
|
}
|
2017-05-13 00:03:56 +00:00
|
|
|
return fmt.Sprintf("port=%d%s, datadir=%s, peers=%d, lights=%d, ethstats=%s, gastarget=%0.3f MGas, gasprice=%0.3f GWei",
|
|
|
|
info.portFull, discv5, info.datadir, info.peersTotal, info.peersLight, info.ethstats, info.gasTarget, info.gasPrice)
|
2017-04-10 23:25:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// checkNode does a health-check against an boot or seal node server to verify
|
|
|
|
// whether it's running, and if yes, whether it's responsive.
|
|
|
|
func checkNode(client *sshClient, network string, boot bool) (*nodeInfos, error) {
|
|
|
|
kind := "bootnode"
|
|
|
|
if !boot {
|
|
|
|
kind = "sealnode"
|
|
|
|
}
|
|
|
|
// Inspect a possible bootnode container on the host
|
|
|
|
infos, err := inspectContainer(client, fmt.Sprintf("%s_%s_1", network, kind))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if !infos.running {
|
|
|
|
return nil, ErrServiceOffline
|
|
|
|
}
|
|
|
|
// Resolve a few types from the environmental variables
|
|
|
|
totalPeers, _ := strconv.Atoi(infos.envvars["TOTAL_PEERS"])
|
|
|
|
lightPeers, _ := strconv.Atoi(infos.envvars["LIGHT_PEERS"])
|
2017-05-13 00:03:56 +00:00
|
|
|
gasTarget, _ := strconv.ParseFloat(infos.envvars["GAS_TARGET"], 64)
|
|
|
|
gasPrice, _ := strconv.ParseFloat(infos.envvars["GAS_PRICE"], 64)
|
2017-04-10 23:25:53 +00:00
|
|
|
|
|
|
|
// Container available, retrieve its node ID and its genesis json
|
|
|
|
var out []byte
|
2017-09-05 13:06:36 +00:00
|
|
|
if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 geth --exec admin.nodeInfo.id attach", network, kind)); err != nil {
|
2017-04-10 23:25:53 +00:00
|
|
|
return nil, ErrServiceUnreachable
|
|
|
|
}
|
|
|
|
id := bytes.Trim(bytes.TrimSpace(out), "\"")
|
|
|
|
|
|
|
|
if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /genesis.json", network, kind)); err != nil {
|
|
|
|
return nil, ErrServiceUnreachable
|
|
|
|
}
|
|
|
|
genesis := bytes.TrimSpace(out)
|
|
|
|
|
|
|
|
keyJSON, keyPass := "", ""
|
|
|
|
if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /signer.json", network, kind)); err == nil {
|
|
|
|
keyJSON = string(bytes.TrimSpace(out))
|
|
|
|
}
|
|
|
|
if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /signer.pass", network, kind)); err == nil {
|
|
|
|
keyPass = string(bytes.TrimSpace(out))
|
|
|
|
}
|
|
|
|
// Run a sanity check to see if the devp2p is reachable
|
|
|
|
port := infos.portmap[infos.envvars["FULL_PORT"]]
|
|
|
|
if err = checkPort(client.server, port); err != nil {
|
|
|
|
log.Warn(fmt.Sprintf("%s devp2p port seems unreachable", strings.Title(kind)), "server", client.server, "port", port, "err", err)
|
|
|
|
}
|
|
|
|
// Assemble and return the useful infos
|
|
|
|
stats := &nodeInfos{
|
|
|
|
genesis: genesis,
|
|
|
|
datadir: infos.volumes["/root/.ethereum"],
|
|
|
|
portFull: infos.portmap[infos.envvars["FULL_PORT"]],
|
|
|
|
portLight: infos.portmap[infos.envvars["LIGHT_PORT"]],
|
|
|
|
peersTotal: totalPeers,
|
|
|
|
peersLight: lightPeers,
|
|
|
|
ethstats: infos.envvars["STATS_NAME"],
|
|
|
|
etherbase: infos.envvars["MINER_NAME"],
|
|
|
|
keyJSON: keyJSON,
|
|
|
|
keyPass: keyPass,
|
2017-05-13 00:03:56 +00:00
|
|
|
gasTarget: gasTarget,
|
|
|
|
gasPrice: gasPrice,
|
2017-04-10 23:25:53 +00:00
|
|
|
}
|
|
|
|
stats.enodeFull = fmt.Sprintf("enode://%s@%s:%d", id, client.address, stats.portFull)
|
|
|
|
if stats.portLight != 0 {
|
|
|
|
stats.enodeLight = fmt.Sprintf("enode://%s@%s:%d?discport=%d", id, client.address, stats.portFull, stats.portLight)
|
|
|
|
}
|
|
|
|
return stats, nil
|
|
|
|
}
|