github.com/tyler-smith/go-ethereum@v1.9.7/cmd/puppeth/module_explorer.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 "fmt" 22 "html/template" 23 "math/rand" 24 "path/filepath" 25 "strconv" 26 "strings" 27 28 "github.com/ethereum/go-ethereum/log" 29 ) 30 31 // explorerDockerfile is the Dockerfile required to run a block explorer. 32 var explorerDockerfile = ` 33 FROM puppeth/blockscout:latest 34 35 ADD genesis.json /genesis.json 36 RUN \ 37 echo 'geth --cache 512 init /genesis.json' > explorer.sh && \ 38 echo $'geth --networkid {{.NetworkID}} --syncmode "full" --gcmode "archive" --port {{.EthPort}} --bootnodes {{.Bootnodes}} --ethstats \'{{.Ethstats}}\' --cache=512 --rpc --rpcapi "net,web3,eth,shh,debug" --rpccorsdomain "*" --rpcvhosts "*" --ws --wsorigins "*" --exitwhensynced' >> explorer.sh && \ 39 echo $'exec geth --networkid {{.NetworkID}} --syncmode "full" --gcmode "archive" --port {{.EthPort}} --bootnodes {{.Bootnodes}} --ethstats \'{{.Ethstats}}\' --cache=512 --rpc --rpcapi "net,web3,eth,shh,debug" --rpccorsdomain "*" --rpcvhosts "*" --ws --wsorigins "*" &' >> explorer.sh && \ 40 echo '/usr/local/bin/docker-entrypoint.sh postgres &' >> explorer.sh && \ 41 echo 'sleep 5' >> explorer.sh && \ 42 echo 'mix do ecto.drop --force, ecto.create, ecto.migrate' >> explorer.sh && \ 43 echo 'mix phx.server' >> explorer.sh 44 45 ENTRYPOINT ["/bin/sh", "explorer.sh"] 46 ` 47 48 // explorerComposefile is the docker-compose.yml file required to deploy and 49 // maintain a block explorer. 50 var explorerComposefile = ` 51 version: '2' 52 services: 53 explorer: 54 build: . 55 image: {{.Network}}/explorer 56 container_name: {{.Network}}_explorer_1 57 ports: 58 - "{{.EthPort}}:{{.EthPort}}" 59 - "{{.EthPort}}:{{.EthPort}}/udp"{{if not .VHost}} 60 - "{{.WebPort}}:4000"{{end}} 61 environment: 62 - ETH_PORT={{.EthPort}} 63 - ETH_NAME={{.EthName}} 64 - BLOCK_TRANSFORMER={{.Transformer}}{{if .VHost}} 65 - VIRTUAL_HOST={{.VHost}} 66 - VIRTUAL_PORT=4000{{end}} 67 volumes: 68 - {{.Datadir}}:/opt/app/.ethereum 69 - {{.DBDir}}:/var/lib/postgresql/data 70 logging: 71 driver: "json-file" 72 options: 73 max-size: "1m" 74 max-file: "10" 75 restart: always 76 ` 77 78 // deployExplorer deploys a new block explorer container to a remote machine via 79 // SSH, docker and docker-compose. If an instance with the specified network name 80 // already exists there, it will be overwritten! 81 func deployExplorer(client *sshClient, network string, bootnodes []string, config *explorerInfos, nocache bool, isClique bool) ([]byte, error) { 82 // Generate the content to upload to the server 83 workdir := fmt.Sprintf("%d", rand.Int63()) 84 files := make(map[string][]byte) 85 86 dockerfile := new(bytes.Buffer) 87 template.Must(template.New("").Parse(explorerDockerfile)).Execute(dockerfile, map[string]interface{}{ 88 "NetworkID": config.node.network, 89 "Bootnodes": strings.Join(bootnodes, ","), 90 "Ethstats": config.node.ethstats, 91 "EthPort": config.node.port, 92 }) 93 files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes() 94 95 transformer := "base" 96 if isClique { 97 transformer = "clique" 98 } 99 composefile := new(bytes.Buffer) 100 template.Must(template.New("").Parse(explorerComposefile)).Execute(composefile, map[string]interface{}{ 101 "Network": network, 102 "VHost": config.host, 103 "Ethstats": config.node.ethstats, 104 "Datadir": config.node.datadir, 105 "DBDir": config.dbdir, 106 "EthPort": config.node.port, 107 "EthName": config.node.ethstats[:strings.Index(config.node.ethstats, ":")], 108 "WebPort": config.port, 109 "Transformer": transformer, 110 }) 111 files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes() 112 files[filepath.Join(workdir, "genesis.json")] = config.node.genesis 113 114 // Upload the deployment files to the remote server (and clean up afterwards) 115 if out, err := client.Upload(files); err != nil { 116 return out, err 117 } 118 defer client.Run("rm -rf " + workdir) 119 120 // Build and deploy the boot or seal node service 121 if nocache { 122 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)) 123 } 124 return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate --timeout 60", workdir, network)) 125 } 126 127 // explorerInfos is returned from a block explorer status check to allow reporting 128 // various configuration parameters. 129 type explorerInfos struct { 130 node *nodeInfos 131 dbdir string 132 host string 133 port int 134 } 135 136 // Report converts the typed struct into a plain string->string map, containing 137 // most - but not all - fields for reporting to the user. 138 func (info *explorerInfos) Report() map[string]string { 139 report := map[string]string{ 140 "Website address ": info.host, 141 "Website listener port ": strconv.Itoa(info.port), 142 "Ethereum listener port ": strconv.Itoa(info.node.port), 143 "Ethstats username": info.node.ethstats, 144 } 145 return report 146 } 147 148 // checkExplorer does a health-check against a block explorer server to verify 149 // whether it's running, and if yes, whether it's responsive. 150 func checkExplorer(client *sshClient, network string) (*explorerInfos, error) { 151 // Inspect a possible explorer container on the host 152 infos, err := inspectContainer(client, fmt.Sprintf("%s_explorer_1", network)) 153 if err != nil { 154 return nil, err 155 } 156 if !infos.running { 157 return nil, ErrServiceOffline 158 } 159 // Resolve the port from the host, or the reverse proxy 160 port := infos.portmap["4000/tcp"] 161 if port == 0 { 162 if proxy, _ := checkNginx(client, network); proxy != nil { 163 port = proxy.port 164 } 165 } 166 if port == 0 { 167 return nil, ErrNotExposed 168 } 169 // Resolve the host from the reverse-proxy and the config values 170 host := infos.envvars["VIRTUAL_HOST"] 171 if host == "" { 172 host = client.server 173 } 174 // Run a sanity check to see if the devp2p is reachable 175 p2pPort := infos.portmap[infos.envvars["ETH_PORT"]+"/tcp"] 176 if err = checkPort(host, p2pPort); err != nil { 177 log.Warn("Explorer node seems unreachable", "server", host, "port", p2pPort, "err", err) 178 } 179 if err = checkPort(host, port); err != nil { 180 log.Warn("Explorer service seems unreachable", "server", host, "port", port, "err", err) 181 } 182 // Assemble and return the useful infos 183 stats := &explorerInfos{ 184 node: &nodeInfos{ 185 datadir: infos.volumes["/opt/app/.ethereum"], 186 port: infos.portmap[infos.envvars["ETH_PORT"]+"/tcp"], 187 ethstats: infos.envvars["ETH_NAME"], 188 }, 189 dbdir: infos.volumes["/var/lib/postgresql/data"], 190 host: host, 191 port: port, 192 } 193 return stats, nil 194 }