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