From 706a1e552c96bf75c60844c1dc28fc83778795fc Mon Sep 17 00:00:00 2001 From: Péter Szilágyi Date: Tue, 11 Apr 2017 02:25:53 +0300 Subject: cmd/puppeth: your Ethereum private network manager (#13854) --- cmd/puppeth/module_node.go | 222 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 cmd/puppeth/module_node.go (limited to 'cmd/puppeth/module_node.go') 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 . + +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 +} -- cgit