github.com/cheng762/platon-go@v1.8.17-0.20190529111256-7deff2d7be26/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/PlatONnetwork/PlatON-Go/common" 30 "github.com/PlatONnetwork/PlatON-Go/log" 31 ) 32 33 // nodeDockerfile is the Dockerfile required to run an Ethereum node. 34 var nodeDockerfile = ` 35 FROM ethereum/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 'platon --cache 512 init /genesis.json' > platon.sh && \{{if .Unlock}} 44 echo 'mkdir -p /root/.ethereum/keystore/ && cp /signer.json /root/.ethereum/keystore/' >> platon.sh && \{{end}} 45 echo $'exec platon --networkid {{.NetworkID}} --cache 512 --port {{.Port}} --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}}' >> platon.sh 46 47 ENTRYPOINT ["/bin/sh", "platon.sh"] 48 ` 49 50 // nodeComposefile is the docker-compose.yml file required to deploy and maintain 51 // an Ethereum node (bootnode or miner 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/.ethereum{{if .Ethashdir}} 63 - {{.Ethashdir}}:/root/.ethash{{end}} 64 environment: 65 - PORT={{.Port}}/tcp 66 - TOTAL_PEERS={{.TotalPeers}} 67 - LIGHT_PEERS={{.LightPeers}} 68 - STATS_NAME={{.Ethstats}} 69 - MINER_NAME={{.Etherbase}} 70 - GAS_TARGET={{.GasTarget}} 71 - GAS_LIMIT={{.GasLimit}} 72 - GAS_PRICE={{.GasPrice}} 73 logging: 74 driver: "json-file" 75 options: 76 max-size: "1m" 77 max-file: "10" 78 restart: always 79 ` 80 81 // deployNode deploys a new Ethereum node container to a remote machine via SSH, 82 // docker and docker-compose. If an instance with the specified network name 83 // already exists there, it will be overwritten! 84 func deployNode(client *sshClient, network string, bootnodes []string, config *nodeInfos, nocache bool) ([]byte, error) { 85 kind := "sealnode" 86 if config.keyJSON == "" && config.etherbase == "" { 87 kind = "bootnode" 88 bootnodes = make([]string, 0) 89 } 90 // Generate the content to upload to the server 91 workdir := fmt.Sprintf("%d", rand.Int63()) 92 files := make(map[string][]byte) 93 94 lightFlag := "" 95 if config.peersLight > 0 { 96 lightFlag = fmt.Sprintf("--lightpeers=%d --lightserv=50", config.peersLight) 97 } 98 dockerfile := new(bytes.Buffer) 99 template.Must(template.New("").Parse(nodeDockerfile)).Execute(dockerfile, map[string]interface{}{ 100 "NetworkID": config.network, 101 "Port": config.port, 102 "Peers": config.peersTotal, 103 "LightFlag": lightFlag, 104 "Bootnodes": strings.Join(bootnodes, ","), 105 "Ethstats": config.ethstats, 106 "Etherbase": config.etherbase, 107 "GasTarget": uint64(1000000 * config.gasTarget), 108 "GasLimit": uint64(1000000 * config.gasLimit), 109 "GasPrice": uint64(1000000000 * config.gasPrice), 110 "Unlock": config.keyJSON != "", 111 }) 112 files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes() 113 114 composefile := new(bytes.Buffer) 115 template.Must(template.New("").Parse(nodeComposefile)).Execute(composefile, map[string]interface{}{ 116 "Type": kind, 117 "Datadir": config.datadir, 118 "Ethashdir": config.ethashdir, 119 "Network": network, 120 "Port": config.port, 121 "TotalPeers": config.peersTotal, 122 "Light": config.peersLight > 0, 123 "LightPeers": config.peersLight, 124 "Ethstats": config.ethstats[:strings.Index(config.ethstats, ":")], 125 "Etherbase": config.etherbase, 126 "GasTarget": config.gasTarget, 127 "GasLimit": config.gasLimit, 128 "GasPrice": config.gasPrice, 129 }) 130 files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes() 131 132 files[filepath.Join(workdir, "genesis.json")] = config.genesis 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 if nocache { 145 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)) 146 } 147 return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate --timeout 60", workdir, network)) 148 } 149 150 // nodeInfos is returned from a boot or seal node status check to allow reporting 151 // various configuration parameters. 152 type nodeInfos struct { 153 genesis []byte 154 network int64 155 datadir string 156 ethashdir string 157 ethstats string 158 port int 159 enode string 160 peersTotal int 161 peersLight int 162 etherbase string 163 keyJSON string 164 keyPass string 165 gasTarget float64 166 gasLimit float64 167 gasPrice float64 168 } 169 170 // Report converts the typed struct into a plain string->string map, containing 171 // most - but not all - fields for reporting to the user. 172 func (info *nodeInfos) Report() map[string]string { 173 report := map[string]string{ 174 "Data directory": info.datadir, 175 "Listener port": strconv.Itoa(info.port), 176 "Peer count (all total)": strconv.Itoa(info.peersTotal), 177 "Peer count (light nodes)": strconv.Itoa(info.peersLight), 178 "Ethstats username": info.ethstats, 179 } 180 if info.gasTarget > 0 { 181 // Miner or signer node 182 report["Gas price (minimum accepted)"] = fmt.Sprintf("%0.3f GWei", info.gasPrice) 183 report["Gas floor (baseline target)"] = fmt.Sprintf("%0.3f MGas", info.gasTarget) 184 report["Gas ceil (target maximum)"] = fmt.Sprintf("%0.3f MGas", info.gasLimit) 185 186 if info.etherbase != "" { 187 // Ethash proof-of-work miner 188 report["Ethash directory"] = info.ethashdir 189 report["Miner account"] = info.etherbase 190 } 191 if info.keyJSON != "" { 192 // Clique proof-of-authority signer 193 var key struct { 194 Address string `json:"address"` 195 } 196 if err := json.Unmarshal([]byte(info.keyJSON), &key); err == nil { 197 report["Signer account"] = common.HexToAddress(key.Address).Hex() 198 } else { 199 log.Error("Failed to retrieve signer address", "err", err) 200 } 201 } 202 } 203 return report 204 } 205 206 // checkNode does a health-check against a boot or seal node server to verify 207 // whether it's running, and if yes, whether it's responsive. 208 func checkNode(client *sshClient, network string, boot bool) (*nodeInfos, error) { 209 kind := "bootnode" 210 if !boot { 211 kind = "sealnode" 212 } 213 // Inspect a possible bootnode container on the host 214 infos, err := inspectContainer(client, fmt.Sprintf("%s_%s_1", network, kind)) 215 if err != nil { 216 return nil, err 217 } 218 if !infos.running { 219 return nil, ErrServiceOffline 220 } 221 // Resolve a few types from the environmental variables 222 totalPeers, _ := strconv.Atoi(infos.envvars["TOTAL_PEERS"]) 223 lightPeers, _ := strconv.Atoi(infos.envvars["LIGHT_PEERS"]) 224 gasTarget, _ := strconv.ParseFloat(infos.envvars["GAS_TARGET"], 64) 225 gasLimit, _ := strconv.ParseFloat(infos.envvars["GAS_LIMIT"], 64) 226 gasPrice, _ := strconv.ParseFloat(infos.envvars["GAS_PRICE"], 64) 227 228 // Container available, retrieve its node ID and its genesis json 229 var out []byte 230 if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 platon --exec admin.nodeInfo.id --cache=16 attach", network, kind)); err != nil { 231 return nil, ErrServiceUnreachable 232 } 233 id := bytes.Trim(bytes.TrimSpace(out), "\"") 234 235 if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /genesis.json", network, kind)); err != nil { 236 return nil, ErrServiceUnreachable 237 } 238 genesis := bytes.TrimSpace(out) 239 240 keyJSON, keyPass := "", "" 241 if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /signer.json", network, kind)); err == nil { 242 keyJSON = string(bytes.TrimSpace(out)) 243 } 244 if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /signer.pass", network, kind)); err == nil { 245 keyPass = string(bytes.TrimSpace(out)) 246 } 247 // Run a sanity check to see if the devp2p is reachable 248 port := infos.portmap[infos.envvars["PORT"]] 249 if err = checkPort(client.server, port); err != nil { 250 log.Warn(fmt.Sprintf("%s devp2p port seems unreachable", strings.Title(kind)), "server", client.server, "port", port, "err", err) 251 } 252 // Assemble and return the useful infos 253 stats := &nodeInfos{ 254 genesis: genesis, 255 datadir: infos.volumes["/root/.ethereum"], 256 ethashdir: infos.volumes["/root/.ethash"], 257 port: port, 258 peersTotal: totalPeers, 259 peersLight: lightPeers, 260 ethstats: infos.envvars["STATS_NAME"], 261 etherbase: infos.envvars["MINER_NAME"], 262 keyJSON: keyJSON, 263 keyPass: keyPass, 264 gasTarget: gasTarget, 265 gasLimit: gasLimit, 266 gasPrice: gasPrice, 267 } 268 stats.enode = fmt.Sprintf("enode://%s@%s:%d", id, client.address, stats.port) 269 270 return stats, nil 271 }