github.com/SmartMeshFoundation/Spectrum@v0.0.0-20220621030607-452a266fee1e/cmd/puppeth/wizard_netstats.go (about) 1 // Copyright 2017 The Spectrum Authors 2 // This file is part of Spectrum. 3 // 4 // Spectrum 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 // Spectrum 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 Spectrum. If not, see <http://www.gnu.org/licenses/>. 16 17 package main 18 19 import ( 20 "encoding/json" 21 "os" 22 "sort" 23 "strings" 24 "sync" 25 26 "github.com/SmartMeshFoundation/Spectrum/core" 27 "github.com/SmartMeshFoundation/Spectrum/log" 28 "github.com/olekukonko/tablewriter" 29 ) 30 31 // networkStats verifies the status of network components and generates a protip 32 // configuration set to give users hints on how to do various tasks. 33 func (w *wizard) networkStats() { 34 if len(w.servers) == 0 { 35 log.Info("No remote machines to gather stats from") 36 return 37 } 38 // Clear out some previous configs to refill from current scan 39 w.conf.ethstats = "" 40 w.conf.bootFull = w.conf.bootFull[:0] 41 w.conf.bootLight = w.conf.bootLight[:0] 42 43 // Iterate over all the specified hosts and check their status 44 var pend sync.WaitGroup 45 46 stats := make(serverStats) 47 for server, pubkey := range w.conf.Servers { 48 pend.Add(1) 49 50 // Gather the service stats for each server concurrently 51 go func(server string, pubkey []byte) { 52 defer pend.Done() 53 54 stat := w.gatherStats(server, pubkey, w.servers[server]) 55 56 // All status checks complete, report and check next server 57 w.lock.Lock() 58 defer w.lock.Unlock() 59 60 delete(w.services, server) 61 for service := range stat.services { 62 w.services[server] = append(w.services[server], service) 63 } 64 stats[server] = stat 65 }(server, pubkey) 66 } 67 pend.Wait() 68 69 // Print any collected stats and return 70 stats.render() 71 } 72 73 // gatherStats gathers service statistics for a particular remote server. 74 func (w *wizard) gatherStats(server string, pubkey []byte, client *sshClient) *serverStat { 75 // Gather some global stats to feed into the wizard 76 var ( 77 genesis string 78 ethstats string 79 bootFull []string 80 bootLight []string 81 ) 82 // Ensure a valid SSH connection to the remote server 83 logger := log.New("server", server) 84 logger.Info("Starting remote server health-check") 85 86 stat := &serverStat{ 87 address: client.address, 88 services: make(map[string]map[string]string), 89 } 90 if client == nil { 91 conn, err := dial(server, pubkey) 92 if err != nil { 93 logger.Error("Failed to establish remote connection", "err", err) 94 stat.failure = err.Error() 95 return stat 96 } 97 client = conn 98 } 99 // Client connected one way or another, run health-checks 100 logger.Debug("Checking for nginx availability") 101 if infos, err := checkNginx(client, w.network); err != nil { 102 if err != ErrServiceUnknown { 103 stat.services["nginx"] = map[string]string{"offline": err.Error()} 104 } 105 } else { 106 stat.services["nginx"] = infos.Report() 107 } 108 logger.Debug("Checking for ethstats availability") 109 if infos, err := checkEthstats(client, w.network); err != nil { 110 if err != ErrServiceUnknown { 111 stat.services["ethstats"] = map[string]string{"offline": err.Error()} 112 } 113 } else { 114 stat.services["ethstats"] = infos.Report() 115 ethstats = infos.config 116 } 117 logger.Debug("Checking for bootnode availability") 118 if infos, err := checkNode(client, w.network, true); err != nil { 119 if err != ErrServiceUnknown { 120 stat.services["bootnode"] = map[string]string{"offline": err.Error()} 121 } 122 } else { 123 stat.services["bootnode"] = infos.Report() 124 125 genesis = string(infos.genesis) 126 bootFull = append(bootFull, infos.enodeFull) 127 if infos.enodeLight != "" { 128 bootLight = append(bootLight, infos.enodeLight) 129 } 130 } 131 logger.Debug("Checking for sealnode availability") 132 if infos, err := checkNode(client, w.network, false); err != nil { 133 if err != ErrServiceUnknown { 134 stat.services["sealnode"] = map[string]string{"offline": err.Error()} 135 } 136 } else { 137 stat.services["sealnode"] = infos.Report() 138 genesis = string(infos.genesis) 139 } 140 logger.Debug("Checking for explorer availability") 141 if infos, err := checkExplorer(client, w.network); err != nil { 142 if err != ErrServiceUnknown { 143 stat.services["explorer"] = map[string]string{"offline": err.Error()} 144 } 145 } else { 146 stat.services["explorer"] = infos.Report() 147 } 148 logger.Debug("Checking for wallet availability") 149 if infos, err := checkWallet(client, w.network); err != nil { 150 if err != ErrServiceUnknown { 151 stat.services["wallet"] = map[string]string{"offline": err.Error()} 152 } 153 } else { 154 stat.services["wallet"] = infos.Report() 155 } 156 logger.Debug("Checking for faucet availability") 157 if infos, err := checkFaucet(client, w.network); err != nil { 158 if err != ErrServiceUnknown { 159 stat.services["faucet"] = map[string]string{"offline": err.Error()} 160 } 161 } else { 162 stat.services["faucet"] = infos.Report() 163 } 164 logger.Debug("Checking for dashboard availability") 165 if infos, err := checkDashboard(client, w.network); err != nil { 166 if err != ErrServiceUnknown { 167 stat.services["dashboard"] = map[string]string{"offline": err.Error()} 168 } 169 } else { 170 stat.services["dashboard"] = infos.Report() 171 } 172 // Feed and newly discovered information into the wizard 173 w.lock.Lock() 174 defer w.lock.Unlock() 175 176 if genesis != "" && w.conf.Genesis == nil { 177 g := new(core.Genesis) 178 if err := json.Unmarshal([]byte(genesis), g); err != nil { 179 log.Error("Failed to parse remote genesis", "err", err) 180 } else { 181 w.conf.Genesis = g 182 } 183 } 184 if ethstats != "" { 185 w.conf.ethstats = ethstats 186 } 187 w.conf.bootFull = append(w.conf.bootFull, bootFull...) 188 w.conf.bootLight = append(w.conf.bootLight, bootLight...) 189 190 return stat 191 } 192 193 // serverStat is a collection of service configuration parameters and health 194 // check reports to print to the user. 195 type serverStat struct { 196 address string 197 failure string 198 services map[string]map[string]string 199 } 200 201 // serverStats is a collection of server stats for multiple hosts. 202 type serverStats map[string]*serverStat 203 204 // render converts the gathered statistics into a user friendly tabular report 205 // and prints it to the standard output. 206 func (stats serverStats) render() { 207 // Start gathering service statistics and config parameters 208 table := tablewriter.NewWriter(os.Stdout) 209 210 table.SetHeader([]string{"Server", "Address", "Service", "Config", "Value"}) 211 table.SetAlignment(tablewriter.ALIGN_LEFT) 212 table.SetColWidth(100) 213 214 // Find the longest lines for all columns for the hacked separator 215 separator := make([]string, 5) 216 for server, stat := range stats { 217 if len(server) > len(separator[0]) { 218 separator[0] = strings.Repeat("-", len(server)) 219 } 220 if len(stat.address) > len(separator[1]) { 221 separator[1] = strings.Repeat("-", len(stat.address)) 222 } 223 for service, configs := range stat.services { 224 if len(service) > len(separator[2]) { 225 separator[2] = strings.Repeat("-", len(service)) 226 } 227 for config, value := range configs { 228 if len(config) > len(separator[3]) { 229 separator[3] = strings.Repeat("-", len(config)) 230 } 231 if len(value) > len(separator[4]) { 232 separator[4] = strings.Repeat("-", len(value)) 233 } 234 } 235 } 236 } 237 // Fill up the server report in alphabetical order 238 servers := make([]string, 0, len(stats)) 239 for server := range stats { 240 servers = append(servers, server) 241 } 242 sort.Strings(servers) 243 244 for i, server := range servers { 245 // Add a separator between all servers 246 if i > 0 { 247 table.Append(separator) 248 } 249 // Fill up the service report in alphabetical order 250 services := make([]string, 0, len(stats[server].services)) 251 for service := range stats[server].services { 252 services = append(services, service) 253 } 254 sort.Strings(services) 255 256 if len(services) == 0 { 257 table.Append([]string{server, stats[server].address, "", "", ""}) 258 } 259 for j, service := range services { 260 // Add an empty line between all services 261 if j > 0 { 262 table.Append([]string{"", "", "", separator[3], separator[4]}) 263 } 264 // Fill up the config report in alphabetical order 265 configs := make([]string, 0, len(stats[server].services[service])) 266 for service := range stats[server].services[service] { 267 configs = append(configs, service) 268 } 269 sort.Strings(configs) 270 271 for k, config := range configs { 272 switch { 273 case j == 0 && k == 0: 274 table.Append([]string{server, stats[server].address, service, config, stats[server].services[service][config]}) 275 case k == 0: 276 table.Append([]string{"", "", service, config, stats[server].services[service][config]}) 277 default: 278 table.Append([]string{"", "", "", config, stats[server].services[service][config]}) 279 } 280 } 281 } 282 } 283 table.Render() 284 } 285 286 // protips contains a collection of network infos to report pro-tips 287 // based on. 288 type protips struct { 289 genesis string 290 network int64 291 bootFull []string 292 bootLight []string 293 ethstats string 294 }