github.com/n1ghtfa1l/go-vnt@v0.6.4-alpha.6/cmd/puppeth/module_node.go (about)

     1  // Copyright 2017 The go-ethereum Authors
     2  // This file is part of go-ethereum.
     3  //
     4  // go-ethereum 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-ethereum 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-ethereum. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package main
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"fmt"
    23  	"math/rand"
    24  	"path/filepath"
    25  	"strconv"
    26  	"strings"
    27  	"text/template"
    28  
    29  	"github.com/vntchain/go-vnt/common"
    30  	"github.com/vntchain/go-vnt/log"
    31  )
    32  
    33  // nodeDockerfile is the Dockerfile required to run an VNT node.
    34  var nodeDockerfile = `
    35  FROM vntchain/client-go:latest
    36  
    37  ADD genesis.json /genesis.json
    38  {{if .Unlock}}
    39  	ADD signer.json /signer.json
    40  	ADD signer.pass /signer.pass
    41  {{end}}
    42  RUN \
    43    echo 'gvnt --cache 512 init /genesis.json' > gvnt.sh && \{{if .Unlock}}
    44  	echo 'mkdir -p /root/.vntchain/keystore/ && cp /signer.json /root/.vntchain/keystore/' >> gvnt.sh && \{{end}}
    45  	echo $'gvnt --networkid {{.NetworkID}} --cache 512 --port {{.Port}} --maxpeers {{.Peers}} {{.LightFlag}} --vntstats \'{{.Vntstats}}\' {{if .Bootnodes}}--bootnodes {{.Bootnodes}}{{end}} {{if .Coinbase}}--coinbase {{.Coinbase}} --produce --producerthreads 1{{end}} {{if .Unlock}}--unlock 0 --password /signer.pass --produce{{end}} --targetgaslimit {{.GasTarget}} --gasprice {{.GasPrice}}' >> gvnt.sh
    46  
    47  ENTRYPOINT ["/bin/sh", "gvnt.sh"]
    48  `
    49  
    50  // nodeComposefile is the docker-compose.yml file required to deploy and maintain
    51  // an VNT node (bootnode or producer for now).
    52  var nodeComposefile = `
    53  version: '2'
    54  services:
    55    {{.Type}}:
    56      build: .
    57      image: {{.Network}}/{{.Type}}
    58      ports:
    59        - "{{.Port}}:{{.Port}}"
    60        - "{{.Port}}:{{.Port}}/udp"
    61      volumes:
    62        - {{.Datadir}}:/root/.vntchain
    63      environment:
    64        - PORT={{.Port}}/tcp
    65        - TOTAL_PEERS={{.TotalPeers}}
    66        - LIGHT_PEERS={{.LightPeers}}
    67        - STATS_NAME={{.Vntstats}}
    68        - PRODUCER_NAME={{.Coinbase}}
    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 VNT 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, bootnodes []string, config *nodeInfos, nocache bool) ([]byte, error) {
    83  	kind := "sealnode"
    84  	if config.keyJSON == "" && config.coinbase == "" {
    85  		kind = "bootnode"
    86  		bootnodes = make([]string, 0)
    87  	}
    88  	// Generate the content to upload to the server
    89  	workdir := fmt.Sprintf("%d", rand.Int63())
    90  	files := make(map[string][]byte)
    91  
    92  	lightFlag := ""
    93  	if config.peersLight > 0 {
    94  		lightFlag = fmt.Sprintf("--lightpeers=%d --lightserv=50", config.peersLight)
    95  	}
    96  	dockerfile := new(bytes.Buffer)
    97  	template.Must(template.New("").Parse(nodeDockerfile)).Execute(dockerfile, map[string]interface{}{
    98  		"NetworkID": config.network,
    99  		"Port":      config.port,
   100  		"Peers":     config.peersTotal,
   101  		"LightFlag": lightFlag,
   102  		"Bootnodes": strings.Join(bootnodes, ","),
   103  		"Vntstats":  config.vntstats,
   104  		"Coinbase":  config.coinbase,
   105  		"GasTarget": uint64(1000000 * config.gasTarget),
   106  		"GasPrice":  uint64(1000000000 * config.gasPrice),
   107  		"Unlock":    config.keyJSON != "",
   108  	})
   109  	files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
   110  
   111  	composefile := new(bytes.Buffer)
   112  	template.Must(template.New("").Parse(nodeComposefile)).Execute(composefile, map[string]interface{}{
   113  		"Type":       kind,
   114  		"Datadir":    config.datadir,
   115  		"Network":    network,
   116  		"Port":       config.port,
   117  		"TotalPeers": config.peersTotal,
   118  		"Light":      config.peersLight > 0,
   119  		"LightPeers": config.peersLight,
   120  		"Vntstats":   config.vntstats[:strings.Index(config.vntstats, ":")],
   121  		"Coinbase":   config.coinbase,
   122  		"GasTarget":  config.gasTarget,
   123  		"GasPrice":   config.gasPrice,
   124  	})
   125  	files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
   126  
   127  	files[filepath.Join(workdir, "genesis.json")] = config.genesis
   128  	if config.keyJSON != "" {
   129  		files[filepath.Join(workdir, "signer.json")] = []byte(config.keyJSON)
   130  		files[filepath.Join(workdir, "signer.pass")] = []byte(config.keyPass)
   131  	}
   132  	// Upload the deployment files to the remote server (and clean up afterwards)
   133  	if out, err := client.Upload(files); err != nil {
   134  		return out, err
   135  	}
   136  	defer client.Run("rm -rf " + workdir)
   137  
   138  	// Build and deploy the boot or seal node service
   139  	if nocache {
   140  		return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate", workdir, network, network))
   141  	}
   142  	return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
   143  }
   144  
   145  // nodeInfos is returned from a boot or seal node status check to allow reporting
   146  // various configuration parameters.
   147  type nodeInfos struct {
   148  	genesis    []byte
   149  	network    int64
   150  	datadir    string
   151  	vntstats   string
   152  	port       int
   153  	vnode      string
   154  	peersTotal int
   155  	peersLight int
   156  	coinbase   string
   157  	keyJSON    string
   158  	keyPass    string
   159  	gasTarget  float64
   160  	gasPrice   float64
   161  }
   162  
   163  // Report converts the typed struct into a plain string->string map, containing
   164  // most - but not all - fields for reporting to the user.
   165  func (info *nodeInfos) Report() map[string]string {
   166  	report := map[string]string{
   167  		"Data directory":           info.datadir,
   168  		"Listener port":            strconv.Itoa(info.port),
   169  		"Peer count (all total)":   strconv.Itoa(info.peersTotal),
   170  		"Peer count (light nodes)": strconv.Itoa(info.peersLight),
   171  		"Vntstats username":        info.vntstats,
   172  	}
   173  	if info.gasTarget > 0 {
   174  		// Producer or signer node
   175  		report["Gas limit (baseline target)"] = fmt.Sprintf("%0.3f MGas", info.gasTarget)
   176  		report["Gas price (minimum accepted)"] = fmt.Sprintf("%0.3f GWei", info.gasPrice)
   177  
   178  		if info.coinbase != "" {
   179  			report["Producer account"] = info.coinbase
   180  		}
   181  		if info.keyJSON != "" {
   182  			var key struct {
   183  				Address string `json:"address"`
   184  			}
   185  			if err := json.Unmarshal([]byte(info.keyJSON), &key); err == nil {
   186  				report["Signer account"] = common.HexToAddress(key.Address).Hex()
   187  			} else {
   188  				log.Error("Failed to retrieve signer address", "err", err)
   189  			}
   190  		}
   191  	}
   192  	return report
   193  }
   194  
   195  // checkNode does a health-check against a boot or seal node server to verify
   196  // whether it's running, and if yes, whether it's responsive.
   197  func checkNode(client *sshClient, network string, boot bool) (*nodeInfos, error) {
   198  	kind := "bootnode"
   199  	if !boot {
   200  		kind = "sealnode"
   201  	}
   202  	// Inspect a possible bootnode container on the host
   203  	infos, err := inspectContainer(client, fmt.Sprintf("%s_%s_1", network, kind))
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  	if !infos.running {
   208  		return nil, ErrServiceOffline
   209  	}
   210  	// Resolve a few types from the environmental variables
   211  	totalPeers, _ := strconv.Atoi(infos.envvars["TOTAL_PEERS"])
   212  	lightPeers, _ := strconv.Atoi(infos.envvars["LIGHT_PEERS"])
   213  	gasTarget, _ := strconv.ParseFloat(infos.envvars["GAS_TARGET"], 64)
   214  	gasPrice, _ := strconv.ParseFloat(infos.envvars["GAS_PRICE"], 64)
   215  
   216  	// Container available, retrieve its node ID and its genesis json
   217  	var out []byte
   218  	if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 gvnt --exec admin.nodeInfo.id attach", network, kind)); err != nil {
   219  		return nil, ErrServiceUnreachable
   220  	}
   221  	id := bytes.Trim(bytes.TrimSpace(out), "\"")
   222  
   223  	if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /genesis.json", network, kind)); err != nil {
   224  		return nil, ErrServiceUnreachable
   225  	}
   226  	genesis := bytes.TrimSpace(out)
   227  
   228  	keyJSON, keyPass := "", ""
   229  	if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /signer.json", network, kind)); err == nil {
   230  		keyJSON = string(bytes.TrimSpace(out))
   231  	}
   232  	if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /signer.pass", network, kind)); err == nil {
   233  		keyPass = string(bytes.TrimSpace(out))
   234  	}
   235  	// Run a sanity check to see if the devp2p is reachable
   236  	port := infos.portmap[infos.envvars["PORT"]]
   237  	if err = checkPort(client.server, port); err != nil {
   238  		log.Warn(fmt.Sprintf("%s devp2p port seems unreachable", strings.Title(kind)), "server", client.server, "port", port, "err", err)
   239  	}
   240  	// Assemble and return the useful infos
   241  	stats := &nodeInfos{
   242  		genesis:    genesis,
   243  		datadir:    infos.volumes["/root/.vntchain"],
   244  		port:       port,
   245  		peersTotal: totalPeers,
   246  		peersLight: lightPeers,
   247  		vntstats:   infos.envvars["STATS_NAME"],
   248  		coinbase:   infos.envvars["PRODUCER_NAME"],
   249  		keyJSON:    keyJSON,
   250  		keyPass:    keyPass,
   251  		gasTarget:  gasTarget,
   252  		gasPrice:   gasPrice,
   253  	}
   254  	stats.vnode = fmt.Sprintf("vnode://%s@%s:%d", id, client.address, stats.port)
   255  
   256  	return stats, nil
   257  }