aboutsummaryrefslogtreecommitdiffstats
path: root/cmd/puppeth/module_node.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/puppeth/module_node.go')
-rw-r--r--cmd/puppeth/module_node.go222
1 files changed, 222 insertions, 0 deletions
diff --git a/cmd/puppeth/module_node.go b/cmd/puppeth/module_node.go
new file mode 100644
index 000000000..78681934d
--- /dev/null
+++ b/cmd/puppeth/module_node.go
@@ -0,0 +1,222 @@
+// 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 = `
+FROM ethereum/client-go:alpine-develop
+
+ADD genesis.json /genesis.json
+{{if .Unlock}}
+ ADD signer.json /signer.json
+ ADD signer.pass /signer.pass
+{{end}}
+RUN \
+ echo '/geth init /genesis.json' > geth.sh && \{{if .Unlock}}
+ echo 'mkdir -p /root/.ethereum/keystore/ && cp /signer.json /root/.ethereum/keystore/' >> geth.sh && \{{end}}
+ echo $'/geth --networkid {{.NetworkID}} --cache 512 --port {{.Port}} --maxpeers {{.Peers}} {{.LightFlag}} --ethstats \'{{.Ethstats}}\' {{if .Bootnodes}}--bootnodes {{.Bootnodes}}{{end}} {{if .Etherbase}}--etherbase {{.Etherbase}} --mine{{end}}{{if .Unlock}}--unlock 0 --password /signer.pass --mine{{end}}' >> geth.sh
+
+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}}
+ 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!
+func deployNode(client *sshClient, network string, bootnodes []string, config *nodeInfos) ([]byte, error) {
+ kind := "sealnode"
+ if config.keyJSON == "" && config.etherbase == "" {
+ kind = "bootnode"
+ bootnodes = make([]string, 0)
+ }
+ // 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,
+ "Bootnodes": strings.Join(bootnodes, ","),
+ "Ethstats": config.ethstats,
+ "Etherbase": config.etherbase,
+ "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,
+ })
+ files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
+
+ //genesisfile, _ := json.MarshalIndent(config.genesis, "", " ")
+ files[filepath.Join(workdir, "genesis.json")] = []byte(config.genesis)
+
+ 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)
+
+ // Build and deploy the bootnode service
+ 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
+}
+
+// String implements the stringer interface.
+func (info *nodeInfos) String() string {
+ discv5 := ""
+ if info.peersLight > 0 {
+ discv5 = fmt.Sprintf(", portv5=%d", info.portLight)
+ }
+ return fmt.Sprintf("port=%d%s, datadir=%s, peers=%d, lights=%d, ethstats=%s", info.portFull, discv5, info.datadir, info.peersTotal, info.peersLight, info.ethstats)
+}
+
+// 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"])
+
+ // Container available, retrieve its node ID and its genesis json
+ var out []byte
+ if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 /geth --exec admin.nodeInfo.id attach", network, kind)); err != nil {
+ 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,
+ }
+ 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
+}