285 lines
8.6 KiB
Go
285 lines
8.6 KiB
Go
// Copyright 2017 The go-ethereum Authors
|
|
// This file is part of go-ethereum.
|
|
//
|
|
// go-ethereum is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// go-ethereum is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/ethereum/go-ethereum/core"
|
|
"github.com/ethereum/go-ethereum/log"
|
|
"github.com/olekukonko/tablewriter"
|
|
)
|
|
|
|
// networkStats verifies the status of network components and generates a protip
|
|
// configuration set to give users hints on how to do various tasks.
|
|
func (w *wizard) networkStats() {
|
|
if len(w.servers) == 0 {
|
|
log.Info("No remote machines to gather stats from")
|
|
return
|
|
}
|
|
// Clear out some previous configs to refill from current scan
|
|
w.conf.ethstats = ""
|
|
w.conf.bootnodes = w.conf.bootnodes[:0]
|
|
|
|
// Iterate over all the specified hosts and check their status
|
|
var pend sync.WaitGroup
|
|
|
|
stats := make(serverStats)
|
|
for server, pubkey := range w.conf.Servers {
|
|
pend.Add(1)
|
|
|
|
// Gather the service stats for each server concurrently
|
|
go func(server string, pubkey []byte) {
|
|
defer pend.Done()
|
|
|
|
stat := w.gatherStats(server, pubkey, w.servers[server])
|
|
|
|
// All status checks complete, report and check next server
|
|
w.lock.Lock()
|
|
defer w.lock.Unlock()
|
|
|
|
delete(w.services, server)
|
|
for service := range stat.services {
|
|
w.services[server] = append(w.services[server], service)
|
|
}
|
|
stats[server] = stat
|
|
}(server, pubkey)
|
|
}
|
|
pend.Wait()
|
|
|
|
// Print any collected stats and return
|
|
stats.render()
|
|
}
|
|
|
|
// gatherStats gathers service statistics for a particular remote server.
|
|
func (w *wizard) gatherStats(server string, pubkey []byte, client *sshClient) *serverStat {
|
|
// Gather some global stats to feed into the wizard
|
|
var (
|
|
genesis string
|
|
ethstats string
|
|
bootnodes []string
|
|
)
|
|
// Ensure a valid SSH connection to the remote server
|
|
logger := log.New("server", server)
|
|
logger.Info("Starting remote server health-check")
|
|
|
|
stat := &serverStat{
|
|
services: make(map[string]map[string]string),
|
|
}
|
|
if client == nil {
|
|
conn, err := dial(server, pubkey)
|
|
if err != nil {
|
|
logger.Error("Failed to establish remote connection", "err", err)
|
|
stat.failure = err.Error()
|
|
return stat
|
|
}
|
|
client = conn
|
|
}
|
|
stat.address = client.address
|
|
|
|
// Client connected one way or another, run health-checks
|
|
logger.Debug("Checking for nginx availability")
|
|
if infos, err := checkNginx(client, w.network); err != nil {
|
|
if err != ErrServiceUnknown {
|
|
stat.services["nginx"] = map[string]string{"offline": err.Error()}
|
|
}
|
|
} else {
|
|
stat.services["nginx"] = infos.Report()
|
|
}
|
|
logger.Debug("Checking for ethstats availability")
|
|
if infos, err := checkEthstats(client, w.network); err != nil {
|
|
if err != ErrServiceUnknown {
|
|
stat.services["ethstats"] = map[string]string{"offline": err.Error()}
|
|
}
|
|
} else {
|
|
stat.services["ethstats"] = infos.Report()
|
|
ethstats = infos.config
|
|
}
|
|
logger.Debug("Checking for bootnode availability")
|
|
if infos, err := checkNode(client, w.network, true); err != nil {
|
|
if err != ErrServiceUnknown {
|
|
stat.services["bootnode"] = map[string]string{"offline": err.Error()}
|
|
}
|
|
} else {
|
|
stat.services["bootnode"] = infos.Report()
|
|
|
|
genesis = string(infos.genesis)
|
|
bootnodes = append(bootnodes, infos.enode)
|
|
}
|
|
logger.Debug("Checking for sealnode availability")
|
|
if infos, err := checkNode(client, w.network, false); err != nil {
|
|
if err != ErrServiceUnknown {
|
|
stat.services["sealnode"] = map[string]string{"offline": err.Error()}
|
|
}
|
|
} else {
|
|
stat.services["sealnode"] = infos.Report()
|
|
genesis = string(infos.genesis)
|
|
}
|
|
logger.Debug("Checking for explorer availability")
|
|
if infos, err := checkExplorer(client, w.network); err != nil {
|
|
if err != ErrServiceUnknown {
|
|
stat.services["explorer"] = map[string]string{"offline": err.Error()}
|
|
}
|
|
} else {
|
|
stat.services["explorer"] = infos.Report()
|
|
}
|
|
logger.Debug("Checking for faucet availability")
|
|
if infos, err := checkFaucet(client, w.network); err != nil {
|
|
if err != ErrServiceUnknown {
|
|
stat.services["faucet"] = map[string]string{"offline": err.Error()}
|
|
}
|
|
} else {
|
|
stat.services["faucet"] = infos.Report()
|
|
}
|
|
logger.Debug("Checking for dashboard availability")
|
|
if infos, err := checkDashboard(client, w.network); err != nil {
|
|
if err != ErrServiceUnknown {
|
|
stat.services["dashboard"] = map[string]string{"offline": err.Error()}
|
|
}
|
|
} else {
|
|
stat.services["dashboard"] = infos.Report()
|
|
}
|
|
// Feed and newly discovered information into the wizard
|
|
w.lock.Lock()
|
|
defer w.lock.Unlock()
|
|
|
|
if genesis != "" && w.conf.Genesis == nil {
|
|
g := new(core.Genesis)
|
|
if err := json.Unmarshal([]byte(genesis), g); err != nil {
|
|
log.Error("Failed to parse remote genesis", "err", err)
|
|
} else {
|
|
w.conf.Genesis = g
|
|
}
|
|
}
|
|
if ethstats != "" {
|
|
w.conf.ethstats = ethstats
|
|
}
|
|
w.conf.bootnodes = append(w.conf.bootnodes, bootnodes...)
|
|
|
|
return stat
|
|
}
|
|
|
|
// serverStat is a collection of service configuration parameters and health
|
|
// check reports to print to the user.
|
|
type serverStat struct {
|
|
address string
|
|
failure string
|
|
services map[string]map[string]string
|
|
}
|
|
|
|
// serverStats is a collection of server stats for multiple hosts.
|
|
type serverStats map[string]*serverStat
|
|
|
|
// render converts the gathered statistics into a user friendly tabular report
|
|
// and prints it to the standard output.
|
|
func (stats serverStats) render() {
|
|
// Start gathering service statistics and config parameters
|
|
table := tablewriter.NewWriter(os.Stdout)
|
|
|
|
table.SetHeader([]string{"Server", "Address", "Service", "Config", "Value"})
|
|
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
|
table.SetColWidth(40)
|
|
|
|
// Find the longest lines for all columns for the hacked separator
|
|
separator := make([]string, 5)
|
|
for server, stat := range stats {
|
|
if len(server) > len(separator[0]) {
|
|
separator[0] = strings.Repeat("-", len(server))
|
|
}
|
|
if len(stat.address) > len(separator[1]) {
|
|
separator[1] = strings.Repeat("-", len(stat.address))
|
|
}
|
|
if len(stat.failure) > len(separator[1]) {
|
|
separator[1] = strings.Repeat("-", len(stat.failure))
|
|
}
|
|
for service, configs := range stat.services {
|
|
if len(service) > len(separator[2]) {
|
|
separator[2] = strings.Repeat("-", len(service))
|
|
}
|
|
for config, value := range configs {
|
|
if len(config) > len(separator[3]) {
|
|
separator[3] = strings.Repeat("-", len(config))
|
|
}
|
|
for _, val := range strings.Split(value, "\n") {
|
|
if len(val) > len(separator[4]) {
|
|
separator[4] = strings.Repeat("-", len(val))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Fill up the server report in alphabetical order
|
|
servers := make([]string, 0, len(stats))
|
|
for server := range stats {
|
|
servers = append(servers, server)
|
|
}
|
|
sort.Strings(servers)
|
|
|
|
for i, server := range servers {
|
|
// Add a separator between all servers
|
|
if i > 0 {
|
|
table.Append(separator)
|
|
}
|
|
// Fill up the service report in alphabetical order
|
|
services := make([]string, 0, len(stats[server].services))
|
|
for service := range stats[server].services {
|
|
services = append(services, service)
|
|
}
|
|
sort.Strings(services)
|
|
|
|
if len(services) == 0 {
|
|
if stats[server].failure != "" {
|
|
table.Append([]string{server, stats[server].failure, "", "", ""})
|
|
} else {
|
|
table.Append([]string{server, stats[server].address, "", "", ""})
|
|
}
|
|
}
|
|
for j, service := range services {
|
|
// Add an empty line between all services
|
|
if j > 0 {
|
|
table.Append([]string{"", "", "", separator[3], separator[4]})
|
|
}
|
|
// Fill up the config report in alphabetical order
|
|
configs := make([]string, 0, len(stats[server].services[service]))
|
|
for service := range stats[server].services[service] {
|
|
configs = append(configs, service)
|
|
}
|
|
sort.Strings(configs)
|
|
|
|
for k, config := range configs {
|
|
for l, value := range strings.Split(stats[server].services[service][config], "\n") {
|
|
switch {
|
|
case j == 0 && k == 0 && l == 0:
|
|
table.Append([]string{server, stats[server].address, service, config, value})
|
|
case k == 0 && l == 0:
|
|
table.Append([]string{"", "", service, config, value})
|
|
case l == 0:
|
|
table.Append([]string{"", "", "", config, value})
|
|
default:
|
|
table.Append([]string{"", "", "", "", value})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
table.Render()
|
|
}
|