mirror of
https://github.com/minio/mc
synced 2026-08-29 14:37:42 -04:00
517 lines
15 KiB
Go
517 lines
15 KiB
Go
// Copyright (c) 2015-2022 MinIO, Inc.
|
|
//
|
|
// This file is part of MinIO Object Storage stack
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program 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 Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"net/url"
|
|
"os"
|
|
"os/signal"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"text/tabwriter"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/minio/cli"
|
|
json "github.com/minio/colorjson"
|
|
"github.com/minio/madmin-go/v3"
|
|
"github.com/minio/mc/pkg/probe"
|
|
"github.com/minio/pkg/v3/console"
|
|
)
|
|
|
|
var pingFlags = []cli.Flag{
|
|
cli.IntFlag{
|
|
Name: "count, c",
|
|
Usage: "perform liveliness check for count number of times",
|
|
},
|
|
cli.IntFlag{
|
|
Name: "error-count, e",
|
|
Usage: "exit after N consecutive ping errors",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "exit, x",
|
|
Usage: "exit when server(s) responds and reports being online",
|
|
},
|
|
cli.IntFlag{
|
|
Name: "interval, i",
|
|
Usage: "wait interval between each request in seconds",
|
|
Value: 1,
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "distributed, a",
|
|
Usage: "ping all the servers in the cluster, use it when you have direct access to nodes/pods",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "node",
|
|
Usage: "ping the specified node",
|
|
},
|
|
}
|
|
|
|
// return latency and liveness probe.
|
|
var pingCmd = cli.Command{
|
|
Name: "ping",
|
|
Usage: "perform liveness check",
|
|
Action: mainPing,
|
|
Before: setGlobalsFromContext,
|
|
OnUsageError: onUsageError,
|
|
Flags: append(pingFlags, globalFlags...),
|
|
HideHelpCommand: true,
|
|
CustomHelpTemplate: `NAME:
|
|
{{.HelpName}} - {{.Usage}}
|
|
|
|
USAGE:
|
|
{{.HelpName}} [FLAGS] TARGET [TARGET...]
|
|
{{if .VisibleFlags}}
|
|
FLAGS:
|
|
{{range .VisibleFlags}}{{.}}
|
|
{{end}}{{end}}
|
|
EXAMPLES:
|
|
1. Return Latency and liveness probe.
|
|
{{.Prompt}} {{.HelpName}} myminio
|
|
|
|
2. Return Latency and liveness probe 5 number of times.
|
|
{{.Prompt}} {{.HelpName}} --count 5 myminio
|
|
|
|
3. Return Latency and liveness with wait interval set to 30 seconds.
|
|
{{.Prompt}} {{.HelpName}} --interval 30 myminio
|
|
|
|
4. Stop pinging when error count > 20.
|
|
{{.Prompt}} {{.HelpName}} --error-count 20 myminio
|
|
`,
|
|
}
|
|
|
|
var stop bool
|
|
|
|
// Validate command line arguments.
|
|
func checkPingSyntax(cliCtx *cli.Context) {
|
|
if !cliCtx.Args().Present() {
|
|
showCommandHelpAndExit(cliCtx, 1) // last argument is exit code
|
|
}
|
|
}
|
|
|
|
// JSON jsonified ping result message.
|
|
func (pr PingResult) JSON() string {
|
|
statusJSONBytes, e := json.MarshalIndent(pr, "", " ")
|
|
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
|
|
|
|
return string(statusJSONBytes)
|
|
}
|
|
|
|
var colorMap = template.FuncMap{
|
|
"colorWhite": color.New(color.FgWhite).SprintfFunc(),
|
|
"colorRed": color.New(color.FgRed).SprintfFunc(),
|
|
"colorGreen": color.New(color.FgGreen).SprintfFunc(),
|
|
}
|
|
|
|
// PingDist is the template for ping result in distributed mode
|
|
const PingDist = `{{$x := .Counter}}{{range .EndPointsStats}}{{if eq "ok " .Status}}{{colorWhite $x}}{{colorWhite ": "}}{{colorWhite .Endpoint.Scheme}}{{colorWhite "://"}}{{colorWhite .Endpoint.Host}}{{"\t"}}{{colorWhite "status="}}{{colorGreen .Status}}{{" "}}{{colorWhite "time="}}{{colorWhite .Time}}{{else}}{{colorRed $x}}{{colorRed ": "}}{{colorRed .Endpoint.Scheme}}{{colorRed "://"}}{{colorRed .Endpoint.Host}}{{"\t"}}{{colorRed "status="}}{{colorRed .Status}}{{" "}}{{colorRed "time="}}{{colorRed .Time}}{{end}}
|
|
{{end}}`
|
|
|
|
// Ping is the template for ping result
|
|
const Ping = `{{$x := .Counter}}{{range .EndPointsStats}}{{if eq "ok " .Status}}{{colorWhite $x}}{{colorWhite ": "}}{{colorWhite .Endpoint.Scheme}}{{colorWhite "://"}}{{colorWhite .Endpoint.Host}}{{"\t"}}{{colorWhite "status="}}{{colorGreen .Status}}{{" "}}{{colorWhite "time="}}{{colorWhite .Time}}{{else}}{{colorRed $x}}{{colorRed ": "}}{{colorRed .Endpoint.Scheme}}{{colorRed "://"}}{{colorRed .Endpoint.Host}}{{"\t"}}{{colorRed "status="}}{{colorRed .Status}}{{" "}}{{colorRed "time="}}{{colorRed .Time}}{{end}}{{end}}`
|
|
|
|
// PingTemplateDist - captures ping template
|
|
var PingTemplateDist = template.Must(template.New("ping-list").Funcs(colorMap).Parse(PingDist))
|
|
|
|
// PingTemplate - captures ping template
|
|
var PingTemplate = template.Must(template.New("ping-list").Funcs(colorMap).Parse(Ping))
|
|
|
|
// String colorized service status message.
|
|
func (pr PingResult) String() string {
|
|
var s strings.Builder
|
|
w := tabwriter.NewWriter(&s, 1, 8, 3, ' ', 0)
|
|
var e error
|
|
if len(pr.EndPointsStats) > 1 {
|
|
e = PingTemplateDist.Execute(w, pr)
|
|
} else {
|
|
e = PingTemplate.Execute(w, pr)
|
|
}
|
|
fatalIf(probe.NewError(e), "Unable to initialize template writer")
|
|
w.Flush()
|
|
return s.String()
|
|
}
|
|
|
|
// EndPointStats - container to hold server ping stats
|
|
type EndPointStats struct {
|
|
Endpoint *url.URL `json:"endpoint"`
|
|
DNS string `json:"dns"`
|
|
Status string `json:"status,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Time string `json:"time"`
|
|
}
|
|
|
|
// PingResult contains ping output
|
|
type PingResult struct {
|
|
Status string `json:"status"`
|
|
Counter string `json:"counter"`
|
|
EndPointsStats []EndPointStats `json:"servers"`
|
|
}
|
|
|
|
// PingSummary Summarizes the results of the ping execution.
|
|
type PingSummary struct {
|
|
Status string `json:"status"`
|
|
// map to contain server stats for all the servers
|
|
ServerMap map[string]ServerStats `json:"serverMap"`
|
|
}
|
|
|
|
// JSON jsonified ping summary message.
|
|
func (ps PingSummary) JSON() string {
|
|
pingJSONBytes, e := json.MarshalIndent(ps, "", " ")
|
|
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
|
|
|
|
return string(pingJSONBytes)
|
|
}
|
|
|
|
// String colorized ping summary message.
|
|
func (ps PingSummary) String() string {
|
|
dspOrder := []col{colGreen} // Header
|
|
for i := 0; i < len(ps.ServerMap); i++ {
|
|
dspOrder = append(dspOrder, colGrey)
|
|
}
|
|
var printColors []*color.Color
|
|
for _, c := range dspOrder {
|
|
printColors = append(printColors, getPrintCol(c))
|
|
}
|
|
tbl := console.NewTable(printColors, []bool{false, false, false, false, false, false}, 0)
|
|
|
|
var builder strings.Builder
|
|
cellText := make([][]string, len(ps.ServerMap)+1)
|
|
cellText[0] = []string{
|
|
"Endpoint",
|
|
"Min",
|
|
"Avg",
|
|
"Max",
|
|
"Error",
|
|
"Count",
|
|
}
|
|
index := 0
|
|
for endpoint, ping := range ps.ServerMap {
|
|
index++
|
|
cellText[index] = []string{
|
|
ping.Endpoint.Scheme + "://" + endpoint,
|
|
trimToTwoDecimal(time.Duration(ping.Min)),
|
|
trimToTwoDecimal(time.Duration(ping.Avg)),
|
|
trimToTwoDecimal(time.Duration(ping.Max)),
|
|
strconv.Itoa(ping.ErrorCount),
|
|
strconv.Itoa(ping.Counter),
|
|
}
|
|
}
|
|
e := tbl.PopulateTable(&builder, cellText)
|
|
fatalIf(probe.NewError(e), "unable to populate the table")
|
|
return builder.String()
|
|
}
|
|
|
|
// ServerStats ping result of each endpoint
|
|
type ServerStats struct {
|
|
Endpoint *url.URL `json:"endpoint"`
|
|
Min uint64 `json:"min"`
|
|
Max uint64 `json:"max"`
|
|
Sum uint64 `json:"sum"`
|
|
Avg uint64 `json:"avg"`
|
|
DNS uint64 `json:"dns"` // last DNS resolving time
|
|
ErrorCount int `json:"errorCount"` // used to keep a track of consecutive errors
|
|
Err string `json:"err"`
|
|
Counter int `json:"counter"` // used to find the average, acts as denominator
|
|
}
|
|
|
|
func fetchAdminInfo(admClnt *madmin.AdminClient) (madmin.InfoMessage, error) {
|
|
ctx, cancel := context.WithTimeout(globalContext, 3*time.Second)
|
|
// Fetch the service status of the specified MinIO server
|
|
info, e := admClnt.ServerInfo(ctx)
|
|
cancel()
|
|
if e == nil {
|
|
return info, nil
|
|
}
|
|
|
|
timer := time.NewTimer(time.Second)
|
|
defer timer.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-globalContext.Done():
|
|
return madmin.InfoMessage{}, globalContext.Err()
|
|
case <-timer.C:
|
|
ctx, cancel := context.WithTimeout(globalContext, 3*time.Second)
|
|
info, e := admClnt.ServerInfo(ctx)
|
|
cancel()
|
|
if e == nil {
|
|
return info, nil
|
|
}
|
|
timer.Reset(time.Second)
|
|
}
|
|
}
|
|
}
|
|
|
|
func filterAdminInfo(admClnt *madmin.AdminClient, nodeName string) (madmin.InfoMessage, error) {
|
|
admInfo, e := fetchAdminInfo(admClnt)
|
|
if e != nil {
|
|
return madmin.InfoMessage{}, e
|
|
}
|
|
if len(admInfo.Servers) <= 0 || nodeName == "" {
|
|
return admInfo, nil
|
|
}
|
|
for _, server := range admInfo.Servers {
|
|
if server.Endpoint == nodeName {
|
|
admInfo.Servers = []madmin.ServerProperties{server}
|
|
return admInfo, nil
|
|
}
|
|
}
|
|
fatalIf(errInvalidArgument().Trace(), "Node "+nodeName+" not exist")
|
|
return madmin.InfoMessage{}, e
|
|
}
|
|
|
|
func ping(ctx context.Context, cliCtx *cli.Context, anonClient *madmin.AnonymousClient, admInfo madmin.InfoMessage, pingSummary PingSummary, index int) {
|
|
var endPointStats []EndPointStats
|
|
var servers []madmin.ServerProperties
|
|
if cliCtx.Bool("distributed") || cliCtx.IsSet("node") {
|
|
servers = admInfo.Servers
|
|
}
|
|
allOK := true
|
|
|
|
for result := range anonClient.Alive(ctx, madmin.AliveOpts{}, servers...) {
|
|
stat := pingStats(cliCtx, result, pingSummary)
|
|
status := "ok "
|
|
if !result.Online {
|
|
status = "failed "
|
|
}
|
|
|
|
allOK = allOK && result.Online
|
|
endPointStat := EndPointStats{
|
|
Endpoint: result.Endpoint,
|
|
DNS: time.Duration(stat.DNS).String(),
|
|
Status: status,
|
|
Error: stat.Err,
|
|
Time: trimToTwoDecimal(result.ResponseTime),
|
|
}
|
|
endPointStats = append(endPointStats, endPointStat)
|
|
pingSummary.ServerMap[result.Endpoint.Host] = stat
|
|
|
|
}
|
|
stop = stop || cliCtx.Bool("exit") && allOK
|
|
|
|
printMsg(PingResult{
|
|
Status: "success",
|
|
Counter: pad(strconv.Itoa(index), " ", 3-len(strconv.Itoa(index)), true),
|
|
EndPointsStats: endPointStats,
|
|
})
|
|
if !stop {
|
|
time.Sleep(time.Duration(cliCtx.Int("interval")) * time.Second)
|
|
}
|
|
}
|
|
|
|
func trimToTwoDecimal(d time.Duration) string {
|
|
var f float64
|
|
var unit string
|
|
switch {
|
|
case d >= time.Second:
|
|
f = float64(d) / float64(time.Second)
|
|
|
|
unit = pad("s", " ", 7-len(fmt.Sprintf("%.02f", f)), false)
|
|
default:
|
|
f = float64(d) / float64(time.Millisecond)
|
|
unit = pad("ms", " ", 6-len(fmt.Sprintf("%.02f", f)), false)
|
|
}
|
|
return fmt.Sprintf("%.02f%s", f, unit)
|
|
}
|
|
|
|
// pad adds the `count` number of p string to string s. left true adds to the
|
|
// left and vice-versa. This is done for proper alignment of ping command
|
|
// ex:- padding 2 white space to right '90.18s' - > '90.18s '
|
|
func pad(s, p string, count int, left bool) string {
|
|
ret := make([]byte, len(p)*count+len(s))
|
|
|
|
if left {
|
|
b := ret[:len(p)*count]
|
|
bp := copy(b, p)
|
|
for bp < len(b) {
|
|
copy(b[bp:], b[:bp])
|
|
bp *= 2
|
|
}
|
|
copy(ret[len(b):], s)
|
|
} else {
|
|
b := ret[len(s) : len(p)*count+len(s)]
|
|
bp := copy(b, p)
|
|
for bp < len(b) {
|
|
copy(b[bp:], b[:bp])
|
|
bp *= 2
|
|
}
|
|
copy(ret[:len(s)], s)
|
|
}
|
|
return string(ret)
|
|
}
|
|
|
|
func pingStats(cliCtx *cli.Context, result madmin.AliveResult, ps PingSummary) ServerStats {
|
|
var errorString string
|
|
var sum, avg, dns uint64
|
|
minPing := uint64(math.MaxUint64)
|
|
var maxPing uint64
|
|
var counter, errorCount int
|
|
|
|
if result.Error != nil {
|
|
errorString = result.Error.Error()
|
|
if stat, ok := ps.ServerMap[result.Endpoint.Host]; ok {
|
|
minPing = stat.Min
|
|
maxPing = stat.Max
|
|
sum = stat.Sum
|
|
counter = stat.Counter
|
|
avg = stat.Avg
|
|
errorCount = stat.ErrorCount + 1
|
|
|
|
} else {
|
|
minPing = 0
|
|
errorCount = 1
|
|
}
|
|
if cliCtx.IsSet("error-count") && errorCount >= cliCtx.Int("error-count") {
|
|
stop = true
|
|
}
|
|
|
|
} else {
|
|
// reset consecutive error count
|
|
errorCount = 0
|
|
if stat, ok := ps.ServerMap[result.Endpoint.Host]; ok {
|
|
var minVal uint64
|
|
if stat.Min == 0 {
|
|
minVal = uint64(result.ResponseTime)
|
|
} else {
|
|
minVal = stat.Min
|
|
}
|
|
minPing = uint64(math.Min(float64(minVal), float64(uint64(result.ResponseTime))))
|
|
maxPing = uint64(math.Max(float64(stat.Max), float64(uint64(result.ResponseTime))))
|
|
sum = stat.Sum + uint64(result.ResponseTime.Nanoseconds())
|
|
counter = stat.Counter + 1
|
|
|
|
} else {
|
|
minPing = uint64(math.Min(float64(minPing), float64(uint64(result.ResponseTime))))
|
|
maxPing = uint64(math.Max(float64(maxPing), float64(uint64(result.ResponseTime))))
|
|
sum = uint64(result.ResponseTime)
|
|
counter = 1
|
|
}
|
|
avg = sum / uint64(counter)
|
|
dns = uint64(result.DNSResolveTime.Nanoseconds())
|
|
}
|
|
return ServerStats{result.Endpoint, minPing, maxPing, sum, avg, dns, errorCount, errorString, counter}
|
|
}
|
|
|
|
func watchSignals(ps PingSummary) {
|
|
c := make(chan os.Signal, 1)
|
|
signal.Notify(c, syscall.SIGTERM, syscall.SIGINT)
|
|
go func() {
|
|
s := <-c
|
|
// Ensure that the table structure is not disrupted when manually canceling.
|
|
fmt.Println("")
|
|
printMsg(ps)
|
|
|
|
// Stop profiling if enabled, this needs to be before canceling the
|
|
// global context to check for any unusual cpu/mem/goroutines usage
|
|
stopProfiling()
|
|
|
|
// Cancel the global context
|
|
globalCancel()
|
|
|
|
var exitCode int
|
|
switch s.String() {
|
|
case "interrupt":
|
|
exitCode = globalCancelExitStatus
|
|
case "killed":
|
|
exitCode = globalKillExitStatus
|
|
case "terminated":
|
|
exitCode = globalTerminatExitStatus
|
|
default:
|
|
exitCode = globalErrorExitStatus
|
|
}
|
|
os.Exit(exitCode)
|
|
}()
|
|
}
|
|
|
|
// mainPing is entry point for ping command.
|
|
func mainPing(cliCtx *cli.Context) error {
|
|
// check 'ping' cli arguments.
|
|
checkPingSyntax(cliCtx)
|
|
|
|
console.SetColor("Info", color.New(color.FgGreen, color.Bold))
|
|
console.SetColor("InfoFail", color.New(color.FgRed, color.Bold))
|
|
|
|
ctx, cancel := context.WithCancel(globalContext)
|
|
defer cancel()
|
|
|
|
aliasedURL := cliCtx.Args().Get(0)
|
|
admClient, err := newAdminClient(aliasedURL)
|
|
fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client for `"+aliasedURL+"`.")
|
|
|
|
anonClient, err := newAnonymousClient(aliasedURL)
|
|
fatalIf(err.Trace(aliasedURL), "Unable to initialize anonymous client for `"+aliasedURL+"`.")
|
|
|
|
var admInfo madmin.InfoMessage
|
|
if cliCtx.Bool("distributed") {
|
|
var e error
|
|
admInfo, e = fetchAdminInfo(admClient)
|
|
fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to get server info")
|
|
}
|
|
if cliCtx.IsSet("node") {
|
|
var e error
|
|
admInfo, e = filterAdminInfo(admClient, cliCtx.String("node"))
|
|
fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to get server info")
|
|
}
|
|
pingSummary := PingSummary{
|
|
ServerMap: make(map[string]ServerStats),
|
|
Status: "success",
|
|
}
|
|
|
|
// stop global signals trap.
|
|
GlobalTrapSignals = false
|
|
watchSignals(pingSummary)
|
|
|
|
index := 1
|
|
if cliCtx.IsSet("count") {
|
|
count := cliCtx.Int("count")
|
|
if count < 1 {
|
|
fatalIf(errInvalidArgument().Trace(cliCtx.Args()...), "ping count cannot be less than 1")
|
|
}
|
|
for index <= count {
|
|
// return if consecutive error count more then specified value
|
|
if stop {
|
|
printMsg(pingSummary)
|
|
return nil
|
|
}
|
|
ping(ctx, cliCtx, anonClient, admInfo, pingSummary, index)
|
|
index++
|
|
}
|
|
} else {
|
|
for {
|
|
select {
|
|
case <-globalContext.Done():
|
|
return globalContext.Err()
|
|
default:
|
|
// return if consecutive error count more then specified value
|
|
if stop {
|
|
printMsg(pingSummary)
|
|
return nil
|
|
}
|
|
ping(ctx, cliCtx, anonClient, admInfo, pingSummary, index)
|
|
index++
|
|
}
|
|
}
|
|
}
|
|
printMsg(pingSummary)
|
|
return nil
|
|
}
|