github.com/sberex/go-sberex@v1.8.2-0.20181113200658-ed96ac38f7d7/cmd/puppeth/module_node.go (about)

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