github.com/codingfuture/orig-energi3@v0.8.4/cmd/puppeth/module_node.go (about)

     1  // Copyright 2018 The Energi Core Authors
     2  // Copyright 2017 The go-ethereum Authors
     3  // This file is part of Energi Core.
     4  //
     5  // Energi Core is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // Energi Core is distributed in the hope that it will be useful,
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    13  // GNU General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU General Public License
    16  // along with Energi Core. If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package main
    19  
    20  import (
    21  	"bytes"
    22  	"encoding/json"
    23  	"fmt"
    24  	"math/rand"
    25  	"path/filepath"
    26  	"strconv"
    27  	"strings"
    28  	"text/template"
    29  
    30  	"github.com/ethereum/go-ethereum/common"
    31  	"github.com/ethereum/go-ethereum/log"
    32  )
    33  
    34  // nodeDockerfile is the Dockerfile required to run an Ethereum node.
    35  var nodeDockerfile = `
    36  FROM ethereum/client-go:latest
    37  
    38  ADD genesis.json /genesis.json
    39  {{if .Unlock}}
    40  	ADD signer.json /signer.json
    41  	ADD signer.pass /signer.pass
    42  {{end}}
    43  RUN \
    44    echo 'energi3 --cache 512 init /genesis.json' > energi3.sh && \{{if .Unlock}}
    45  	echo 'mkdir -p /root/.ethereum/keystore/ && cp /signer.json /root/.ethereum/keystore/' >> energi3.sh && \{{end}}
    46  	echo $'exec energi3 --networkid {{.NetworkID}} --cache 512 --port {{.Port}} --nat extip:{{.IP}} --maxpeers {{.Peers}} {{.LightFlag}} --ethstats \'{{.Ethstats}}\' {{if .Bootnodes}}--bootnodes {{.Bootnodes}}{{end}} {{if .Etherbase}}--miner.etherbase {{.Etherbase}} --mine --miner.threads 1{{end}} {{if .Unlock}}--unlock 0 --password /signer.pass --mine{{end}} --miner.gastarget {{.GasTarget}} --miner.gaslimit {{.GasLimit}} --miner.gasprice {{.GasPrice}}' >> energi3.sh
    47  
    48  ENTRYPOINT ["/bin/sh", "energi3.sh"]
    49  `
    50  
    51  // nodeComposefile is the docker-compose.yml file required to deploy and maintain
    52  // an Ethereum node (bootnode or miner for now).
    53  var nodeComposefile = `
    54  version: '2'
    55  services:
    56    {{.Type}}:
    57      build: .
    58      image: {{.Network}}/{{.Type}}
    59      container_name: {{.Network}}_{{.Type}}_1
    60      ports:
    61        - "{{.Port}}:{{.Port}}"
    62        - "{{.Port}}:{{.Port}}/udp"
    63      volumes:
    64        - {{.Datadir}}:/root/.ethereum{{if .Ethashdir}}
    65        - {{.Ethashdir}}:/root/.ethash{{end}}
    66      environment:
    67        - PORT={{.Port}}/tcp
    68        - TOTAL_PEERS={{.TotalPeers}}
    69        - LIGHT_PEERS={{.LightPeers}}
    70        - STATS_NAME={{.Ethstats}}
    71        - MINER_NAME={{.Etherbase}}
    72        - GAS_TARGET={{.GasTarget}}
    73        - GAS_LIMIT={{.GasLimit}}
    74        - GAS_PRICE={{.GasPrice}}
    75      logging:
    76        driver: "json-file"
    77        options:
    78          max-size: "1m"
    79          max-file: "10"
    80      restart: always
    81  `
    82  
    83  // deployNode deploys a new Ethereum node container to a remote machine via SSH,
    84  // docker and docker-compose. If an instance with the specified network name
    85  // already exists there, it will be overwritten!
    86  func deployNode(client *sshClient, network string, bootnodes []string, config *nodeInfos, nocache bool) ([]byte, error) {
    87  	kind := "sealnode"
    88  	if config.keyJSON == "" && config.etherbase == "" {
    89  		kind = "bootnode"
    90  		bootnodes = make([]string, 0)
    91  	}
    92  	// Generate the content to upload to the server
    93  	workdir := fmt.Sprintf("%d", rand.Int63())
    94  	files := make(map[string][]byte)
    95  
    96  	lightFlag := ""
    97  	if config.peersLight > 0 {
    98  		lightFlag = fmt.Sprintf("--lightpeers=%d --lightserv=50", config.peersLight)
    99  	}
   100  	dockerfile := new(bytes.Buffer)
   101  	template.Must(template.New("").Parse(nodeDockerfile)).Execute(dockerfile, map[string]interface{}{
   102  		"NetworkID": config.network,
   103  		"Port":      config.port,
   104  		"IP":        client.address,
   105  		"Peers":     config.peersTotal,
   106  		"LightFlag": lightFlag,
   107  		"Bootnodes": strings.Join(bootnodes, ","),
   108  		"Ethstats":  config.ethstats,
   109  		"Etherbase": config.etherbase,
   110  		"GasTarget": uint64(1000000 * config.gasTarget),
   111  		"GasLimit":  uint64(1000000 * config.gasLimit),
   112  		"GasPrice":  uint64(1000000000 * config.gasPrice),
   113  		"Unlock":    config.keyJSON != "",
   114  	})
   115  	files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
   116  
   117  	composefile := new(bytes.Buffer)
   118  	template.Must(template.New("").Parse(nodeComposefile)).Execute(composefile, map[string]interface{}{
   119  		"Type":       kind,
   120  		"Datadir":    config.datadir,
   121  		"Ethashdir":  config.ethashdir,
   122  		"Network":    network,
   123  		"Port":       config.port,
   124  		"TotalPeers": config.peersTotal,
   125  		"Light":      config.peersLight > 0,
   126  		"LightPeers": config.peersLight,
   127  		"Ethstats":   config.ethstats[:strings.Index(config.ethstats, ":")],
   128  		"Etherbase":  config.etherbase,
   129  		"GasTarget":  config.gasTarget,
   130  		"GasLimit":   config.gasLimit,
   131  		"GasPrice":   config.gasPrice,
   132  	})
   133  	files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
   134  
   135  	files[filepath.Join(workdir, "genesis.json")] = config.genesis
   136  	if config.keyJSON != "" {
   137  		files[filepath.Join(workdir, "signer.json")] = []byte(config.keyJSON)
   138  		files[filepath.Join(workdir, "signer.pass")] = []byte(config.keyPass)
   139  	}
   140  	// Upload the deployment files to the remote server (and clean up afterwards)
   141  	if out, err := client.Upload(files); err != nil {
   142  		return out, err
   143  	}
   144  	defer client.Run("rm -rf " + workdir)
   145  
   146  	// Build and deploy the boot or seal node service
   147  	if nocache {
   148  		return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate --timeout 60", workdir, network, network))
   149  	}
   150  	return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate --timeout 60", workdir, network))
   151  }
   152  
   153  // nodeInfos is returned from a boot or seal node status check to allow reporting
   154  // various configuration parameters.
   155  type nodeInfos struct {
   156  	genesis    []byte
   157  	network    int64
   158  	datadir    string
   159  	ethashdir  string
   160  	ethstats   string
   161  	port       int
   162  	enode      string
   163  	peersTotal int
   164  	peersLight int
   165  	etherbase  string
   166  	keyJSON    string
   167  	keyPass    string
   168  	gasTarget  float64
   169  	gasLimit   float64
   170  	gasPrice   float64
   171  }
   172  
   173  // Report converts the typed struct into a plain string->string map, containing
   174  // most - but not all - fields for reporting to the user.
   175  func (info *nodeInfos) Report() map[string]string {
   176  	report := map[string]string{
   177  		"Data directory":           info.datadir,
   178  		"Listener port":            strconv.Itoa(info.port),
   179  		"Peer count (all total)":   strconv.Itoa(info.peersTotal),
   180  		"Peer count (light nodes)": strconv.Itoa(info.peersLight),
   181  		"Ethstats username":        info.ethstats,
   182  	}
   183  	if info.gasTarget > 0 {
   184  		// Miner or signer node
   185  		report["Gas price (minimum accepted)"] = fmt.Sprintf("%0.3f GWei", info.gasPrice)
   186  		report["Gas floor (baseline target)"] = fmt.Sprintf("%0.3f MGas", info.gasTarget)
   187  		report["Gas ceil  (target maximum)"] = fmt.Sprintf("%0.3f MGas", info.gasLimit)
   188  
   189  		if info.etherbase != "" {
   190  			// Ethash proof-of-work miner
   191  			report["Ethash directory"] = info.ethashdir
   192  			report["Miner account"] = info.etherbase
   193  		}
   194  		if info.keyJSON != "" {
   195  			// Clique proof-of-authority signer
   196  			var key struct {
   197  				Address string `json:"address"`
   198  			}
   199  			if err := json.Unmarshal([]byte(info.keyJSON), &key); err == nil {
   200  				report["Signer account"] = common.HexToAddress(key.Address).Hex()
   201  			} else {
   202  				log.Error("Failed to retrieve signer address", "err", err)
   203  			}
   204  		}
   205  	}
   206  	return report
   207  }
   208  
   209  // checkNode does a health-check against a boot or seal node server to verify
   210  // whether it's running, and if yes, whether it's responsive.
   211  func checkNode(client *sshClient, network string, boot bool) (*nodeInfos, error) {
   212  	kind := "bootnode"
   213  	if !boot {
   214  		kind = "sealnode"
   215  	}
   216  	// Inspect a possible bootnode container on the host
   217  	infos, err := inspectContainer(client, fmt.Sprintf("%s_%s_1", network, kind))
   218  	if err != nil {
   219  		return nil, err
   220  	}
   221  	if !infos.running {
   222  		return nil, ErrServiceOffline
   223  	}
   224  	// Resolve a few types from the environmental variables
   225  	totalPeers, _ := strconv.Atoi(infos.envvars["TOTAL_PEERS"])
   226  	lightPeers, _ := strconv.Atoi(infos.envvars["LIGHT_PEERS"])
   227  	gasTarget, _ := strconv.ParseFloat(infos.envvars["GAS_TARGET"], 64)
   228  	gasLimit, _ := strconv.ParseFloat(infos.envvars["GAS_LIMIT"], 64)
   229  	gasPrice, _ := strconv.ParseFloat(infos.envvars["GAS_PRICE"], 64)
   230  
   231  	// Container available, retrieve its node ID and its genesis json
   232  	var out []byte
   233  	if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 energi3 --exec admin.nodeInfo.enode --cache=16 attach", network, kind)); err != nil {
   234  		return nil, ErrServiceUnreachable
   235  	}
   236  	enode := bytes.Trim(bytes.TrimSpace(out), "\"")
   237  
   238  	if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /genesis.json", network, kind)); err != nil {
   239  		return nil, ErrServiceUnreachable
   240  	}
   241  	genesis := bytes.TrimSpace(out)
   242  
   243  	keyJSON, keyPass := "", ""
   244  	if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /signer.json", network, kind)); err == nil {
   245  		keyJSON = string(bytes.TrimSpace(out))
   246  	}
   247  	if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /signer.pass", network, kind)); err == nil {
   248  		keyPass = string(bytes.TrimSpace(out))
   249  	}
   250  	// Run a sanity check to see if the devp2p is reachable
   251  	port := infos.portmap[infos.envvars["PORT"]]
   252  	if err = checkPort(client.server, port); err != nil {
   253  		log.Warn(fmt.Sprintf("%s devp2p port seems unreachable", strings.Title(kind)), "server", client.server, "port", port, "err", err)
   254  	}
   255  	// Assemble and return the useful infos
   256  	stats := &nodeInfos{
   257  		genesis:    genesis,
   258  		datadir:    infos.volumes["/root/.ethereum"],
   259  		ethashdir:  infos.volumes["/root/.ethash"],
   260  		port:       port,
   261  		peersTotal: totalPeers,
   262  		peersLight: lightPeers,
   263  		ethstats:   infos.envvars["STATS_NAME"],
   264  		etherbase:  infos.envvars["MINER_NAME"],
   265  		keyJSON:    keyJSON,
   266  		keyPass:    keyPass,
   267  		gasTarget:  gasTarget,
   268  		gasLimit:   gasLimit,
   269  		gasPrice:   gasPrice,
   270  	}
   271  	stats.enode = string(enode)
   272  
   273  	return stats, nil
   274  }