github.com/sberex/go-sberex@v1.8.2-0.20181113200658-ed96ac38f7d7/cmd/puppeth/module_explorer.go (about) 1 // This file is part of the go-sberex library. The go-sberex library is 2 // free software: you can redistribute it and/or modify it under the terms 3 // of the GNU Lesser General Public License as published by the Free 4 // Software Foundation, either version 3 of the License, or (at your option) 5 // any later version. 6 // 7 // The go-sberex library is distributed in the hope that it will be useful, 8 // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser 10 // General Public License <http://www.gnu.org/licenses/> for more details. 11 12 package main 13 14 import ( 15 "bytes" 16 "fmt" 17 "html/template" 18 "math/rand" 19 "path/filepath" 20 "strconv" 21 "strings" 22 23 "github.com/Sberex/go-sberex/log" 24 ) 25 26 // explorerDockerfile is the Dockerfile required to run a block explorer. 27 var explorerDockerfile = ` 28 FROM puppeth/explorer:latest 29 30 ADD ethstats.json /ethstats.json 31 ADD chain.json /chain.json 32 33 RUN \ 34 echo '(cd ../eth-net-intelligence-api && pm2 start /ethstats.json)' > explorer.sh && \ 35 echo '(cd ../etherchain-light && npm start &)' >> explorer.sh && \ 36 echo '/parity/parity --chain=/chain.json --port={{.NodePort}} --tracing=on --fat-db=on --pruning=archive' >> explorer.sh 37 38 ENTRYPOINT ["/bin/sh", "explorer.sh"] 39 ` 40 41 // explorerEthstats is the configuration file for the ethstats javascript client. 42 var explorerEthstats = `[ 43 { 44 "name" : "node-app", 45 "script" : "app.js", 46 "log_date_format" : "YYYY-MM-DD HH:mm Z", 47 "merge_logs" : false, 48 "watch" : false, 49 "max_restarts" : 10, 50 "exec_interpreter" : "node", 51 "exec_mode" : "fork_mode", 52 "env": 53 { 54 "NODE_ENV" : "production", 55 "RPC_HOST" : "localhost", 56 "RPC_PORT" : "8545", 57 "LISTENING_PORT" : "{{.Port}}", 58 "INSTANCE_NAME" : "{{.Name}}", 59 "CONTACT_DETAILS" : "", 60 "WS_SERVER" : "{{.Host}}", 61 "WS_SECRET" : "{{.Secret}}", 62 "VERBOSITY" : 2 63 } 64 } 65 ]` 66 67 // explorerComposefile is the docker-compose.yml file required to deploy and 68 // maintain a block explorer. 69 var explorerComposefile = ` 70 version: '2' 71 services: 72 explorer: 73 build: . 74 image: {{.Network}}/explorer 75 ports: 76 - "{{.NodePort}}:{{.NodePort}}" 77 - "{{.NodePort}}:{{.NodePort}}/udp"{{if not .VHost}} 78 - "{{.WebPort}}:3000"{{end}} 79 volumes: 80 - {{.Datadir}}:/root/.local/share/io.parity.sberex 81 environment: 82 - NODE_PORT={{.NodePort}}/tcp 83 - STATS={{.Ethstats}}{{if .VHost}} 84 - VIRTUAL_HOST={{.VHost}} 85 - VIRTUAL_PORT=3000{{end}} 86 logging: 87 driver: "json-file" 88 options: 89 max-size: "1m" 90 max-file: "10" 91 restart: always 92 ` 93 94 // deployExplorer deploys a new block explorer container to a remote machine via 95 // SSH, docker and docker-compose. If an instance with the specified network name 96 // already exists there, it will be overwritten! 97 func deployExplorer(client *sshClient, network string, chainspec []byte, config *explorerInfos, nocache bool) ([]byte, error) { 98 // Generate the content to upload to the server 99 workdir := fmt.Sprintf("%d", rand.Int63()) 100 files := make(map[string][]byte) 101 102 dockerfile := new(bytes.Buffer) 103 template.Must(template.New("").Parse(explorerDockerfile)).Execute(dockerfile, map[string]interface{}{ 104 "NodePort": config.nodePort, 105 }) 106 files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes() 107 108 ethstats := new(bytes.Buffer) 109 template.Must(template.New("").Parse(explorerEthstats)).Execute(ethstats, map[string]interface{}{ 110 "Port": config.nodePort, 111 "Name": config.ethstats[:strings.Index(config.ethstats, ":")], 112 "Secret": config.ethstats[strings.Index(config.ethstats, ":")+1 : strings.Index(config.ethstats, "@")], 113 "Host": config.ethstats[strings.Index(config.ethstats, "@")+1:], 114 }) 115 files[filepath.Join(workdir, "ethstats.json")] = ethstats.Bytes() 116 117 composefile := new(bytes.Buffer) 118 template.Must(template.New("").Parse(explorerComposefile)).Execute(composefile, map[string]interface{}{ 119 "Datadir": config.datadir, 120 "Network": network, 121 "NodePort": config.nodePort, 122 "VHost": config.webHost, 123 "WebPort": config.webPort, 124 "Ethstats": config.ethstats[:strings.Index(config.ethstats, ":")], 125 }) 126 files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes() 127 128 files[filepath.Join(workdir, "chain.json")] = chainspec 129 130 // Upload the deployment files to the remote server (and clean up afterwards) 131 if out, err := client.Upload(files); err != nil { 132 return out, err 133 } 134 defer client.Run("rm -rf " + workdir) 135 136 // Build and deploy the boot or seal node service 137 if nocache { 138 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)) 139 } 140 return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network)) 141 } 142 143 // explorerInfos is returned from a block explorer status check to allow reporting 144 // various configuration parameters. 145 type explorerInfos struct { 146 datadir string 147 ethstats string 148 nodePort int 149 webHost string 150 webPort int 151 } 152 153 // Report converts the typed struct into a plain string->string map, containing 154 // most - but not all - fields for reporting to the user. 155 func (info *explorerInfos) Report() map[string]string { 156 report := map[string]string{ 157 "Data directory": info.datadir, 158 "Node listener port ": strconv.Itoa(info.nodePort), 159 "Ethstats username": info.ethstats, 160 "Website address ": info.webHost, 161 "Website listener port ": strconv.Itoa(info.webPort), 162 } 163 return report 164 } 165 166 // checkExplorer does a health-check against an block explorer server to verify 167 // whether it's running, and if yes, whether it's responsive. 168 func checkExplorer(client *sshClient, network string) (*explorerInfos, error) { 169 // Inspect a possible block explorer container on the host 170 infos, err := inspectContainer(client, fmt.Sprintf("%s_explorer_1", network)) 171 if err != nil { 172 return nil, err 173 } 174 if !infos.running { 175 return nil, ErrServiceOffline 176 } 177 // Resolve the port from the host, or the reverse proxy 178 webPort := infos.portmap["3000/tcp"] 179 if webPort == 0 { 180 if proxy, _ := checkNginx(client, network); proxy != nil { 181 webPort = proxy.port 182 } 183 } 184 if webPort == 0 { 185 return nil, ErrNotExposed 186 } 187 // Resolve the host from the reverse-proxy and the config values 188 host := infos.envvars["VIRTUAL_HOST"] 189 if host == "" { 190 host = client.server 191 } 192 // Run a sanity check to see if the devp2p is reachable 193 nodePort := infos.portmap[infos.envvars["NODE_PORT"]] 194 if err = checkPort(client.server, nodePort); err != nil { 195 log.Warn(fmt.Sprintf("Explorer devp2p port seems unreachable"), "server", client.server, "port", nodePort, "err", err) 196 } 197 // Assemble and return the useful infos 198 stats := &explorerInfos{ 199 datadir: infos.volumes["/root/.local/share/io.parity.sberex"], 200 nodePort: nodePort, 201 webHost: host, 202 webPort: webPort, 203 ethstats: infos.envvars["STATS"], 204 } 205 return stats, nil 206 }