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 }