Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 48 additions & 36 deletions internal/cli/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/spf13/cobra"

"github.com/abhijitkrm/cometcli/internal/config"
"github.com/abhijitkrm/cometcli/internal/toolkit"
)

Expand All @@ -35,11 +36,29 @@ func num(v any) int64 {
return 0
}

// signsBlocks reports whether doctor should check signing and jailing: a
// validator, a profile with no role set, or any node saved with a valoper.
// Archive, RPC and sentry nodes don't sign, so those checks don't apply.
func signsBlocks(p *config.Profile) bool {
return p.IsValidator() || p.Role == "" || p.Metadata["valoper"] != ""
}

// icon is a check's mark: a warning is "!" even when it has detail text.
func icon(ch Check) string {
switch {
case ch.Warn:
return "!"
case !ch.OK:
return "✗"
}
return "✓"
}

// DoctorCmd runs the full node health checklist.
func DoctorCmd(reg *toolkit.Registry) *cobra.Command {
return &cobra.Command{
Use: "doctor",
Short: "Full validator health checklist",
Short: "Full node health checklist",
RunE: func(cmd *cobra.Command, _ []string) error {
c, err := NewCtx(cmd, true)
if err != nil {
Expand Down Expand Up @@ -101,31 +120,33 @@ func DoctorCmd(reg *toolkit.Registry) *cobra.Command {
bad := num(r.Data["bad"])
return Check{OK: bad == 0, Detail: fmt.Sprintf("%d problems", bad)}
})
run("signing health", "val.signing", nil, func(r *toolkit.Result, err error) Check {
if err != nil {
return Check{Warn: true, Detail: "skipped: " + err.Error()}
}
missed := num(r.Data["missed"])
window := num(r.Data["window"])
tomb, _ := r.Data["tombstoned"].(bool)
if tomb {
return Check{OK: false, Detail: "TOMBSTONED — double-sign slashed"}
}
if window > 0 && missed > window/10 {
return Check{OK: false, Detail: fmt.Sprintf("missing %d/%d blocks", missed, window)}
}
return Check{OK: true, Detail: fmt.Sprintf("missed %d/%d", missed, window)}
})
run("jail status", "val.status", nil, func(r *toolkit.Result, err error) Check {
if err != nil {
return Check{Warn: true, Detail: "skipped: " + err.Error()}
}
j, _ := r.Data["jailed"].(bool)
if j {
return Check{OK: false, Detail: "JAILED — investigate + `cometcli val unjail`"}
}
return Check{OK: true, Detail: "not jailed"}
})
if signsBlocks(c.Profile) {
run("signing health", "val.signing", nil, func(r *toolkit.Result, err error) Check {
if err != nil {
return Check{Warn: true, Detail: "skipped: " + err.Error()}
}
missed := num(r.Data["missed"])
window := num(r.Data["window"])
tomb, _ := r.Data["tombstoned"].(bool)
if tomb {
return Check{OK: false, Detail: "TOMBSTONED — double-sign slashed"}
}
if window > 0 && missed > window/10 {
return Check{OK: false, Detail: fmt.Sprintf("missing %d/%d blocks", missed, window)}
}
return Check{OK: true, Detail: fmt.Sprintf("missed %d/%d", missed, window)}
})
run("jail status", "val.status", nil, func(r *toolkit.Result, err error) Check {
if err != nil {
return Check{Warn: true, Detail: "skipped: " + err.Error()}
}
j, _ := r.Data["jailed"].(bool)
if j {
return Check{OK: false, Detail: "JAILED — investigate + `cometcli val unjail`"}
}
return Check{OK: true, Detail: "not jailed"}
})
}
run("upgrade pending", "chain.upgrade-plan", nil, func(r *toolkit.Result, err error) Check {
if err != nil {
return Check{Warn: true, Detail: "skipped: " + err.Error()}
Expand Down Expand Up @@ -203,16 +224,7 @@ func DoctorCmd(reg *toolkit.Registry) *cobra.Command {
}
var b strings.Builder
for _, ch := range checks {
icon := "✓"
switch {
case !ch.OK && ch.Detail != "":
icon = "✗"
case ch.Warn:
icon = "!"
case !ch.OK:
icon = "✗"
}
fmt.Fprintf(&b, "%s %-18s %s\n", icon, ch.Name, ch.Detail)
fmt.Fprintf(&b, "%s %-18s %s\n", icon(ch), ch.Name, ch.Detail)
}
fmt.Fprintf(&b, "\n%d failures, %d warnings\n", fails, warns)
fmt.Fprint(c.Out, b.String())
Expand Down
45 changes: 45 additions & 0 deletions internal/cli/doctor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package cli

import (
"testing"

"github.com/abhijitkrm/cometcli/internal/config"
)

func TestSignsBlocks(t *testing.T) {
for _, tc := range []struct {
role, valoper string
want bool
}{
{"validator", "", true},
{"", "", true}, // role unset: could be a validator
{"archive", "", false},
{"rpc", "", false},
{"sentry", "", false},
{"archive", "cosmosvaloper1x", true}, // saved with a valoper
} {
p := &config.Profile{Role: tc.role, Metadata: map[string]string{}}
if tc.valoper != "" {
p.Metadata["valoper"] = tc.valoper
}
if got := signsBlocks(p); got != tc.want {
t.Errorf("role %q valoper %q: got %v, want %v", tc.role, tc.valoper, got, tc.want)
}
}
}

func TestDoctorIcon(t *testing.T) {
for _, tc := range []struct {
ch Check
want string
}{
{Check{OK: true}, "✓"},
{Check{OK: false, Detail: "0 peers"}, "✗"},
{Check{Warn: true, Detail: "skipped: no route"}, "!"}, // a warning with detail is not a failure
{Check{Warn: true}, "!"},
} {
if got := icon(tc.ch); got != tc.want {
t.Errorf("%+v: got %s, want %s", tc.ch, got, tc.want)
}
}
}
Loading