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