github.com/waltonchain/waltonchain_gwtc_src@v1.1.4-0.20201225072101-8a298c95a819/cmd/puppeth/module_node.go (about)

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