Pulse/internal/ceph/collector_test.go

471 lines
14 KiB
Go

package ceph
import (
"context"
"errors"
"strings"
"testing"
)
func withCommandRunner(t *testing.T, fn func(ctx context.Context, name string, args ...string) ([]byte, []byte, error)) {
t.Helper()
orig := commandRunner
commandRunner = fn
t.Cleanup(func() { commandRunner = orig })
}
func TestCommandRunner_Default(t *testing.T) {
stdout, stderr, err := commandRunner(context.Background(), "sh", "-c", "true")
if err != nil {
t.Fatalf("commandRunner error: %v", err)
}
if len(stdout) != 0 {
t.Fatalf("unexpected stdout: %q", string(stdout))
}
if len(stderr) != 0 {
t.Fatalf("unexpected stderr: %q", string(stderr))
}
}
func TestParseStatus(t *testing.T) {
data := []byte(`{
"fsid":"fsid-123",
"health":{
"status":"HEALTH_WARN",
"checks":{
"OSD_DOWN":{
"severity":"HEALTH_WARN",
"summary":{"message":"1 osd down"},
"detail":[{"message":"osd.1 is down"}]
}
}
},
"monmap":{"epoch":7,"mons":[{"name":"a","rank":0,"addr":"10.0.0.1"}]},
"mgrmap":{"available":true,"active_name":"mgr-a","standbys":[{"name":"mgr-b"}]},
"osdmap":{"epoch":3,"num_osds":3,"num_up_osds":2,"num_in_osds":1},
"pgmap":{
"num_pgs":64,
"bytes_total":1000,
"bytes_used":250,
"bytes_avail":750,
"data_bytes":200,
"degraded_ratio":0.1,
"misplaced_ratio":0.2,
"read_bytes_sec":1,
"write_bytes_sec":2,
"read_op_per_sec":3,
"write_op_per_sec":4
}
}`)
status, err := parseStatus(data)
if err != nil {
t.Fatalf("parseStatus returned error: %v", err)
}
if status.FSID != "fsid-123" {
t.Fatalf("expected FSID fsid-123, got %q", status.FSID)
}
if status.Health.Status != "HEALTH_WARN" {
t.Fatalf("expected HEALTH_WARN, got %q", status.Health.Status)
}
check, ok := status.Health.Checks["OSD_DOWN"]
if !ok || check.Severity != "HEALTH_WARN" || check.Message != "1 osd down" || len(check.Detail) != 1 {
t.Fatalf("unexpected parsed health checks: %+v", status.Health.Checks)
}
if status.MonMap.NumMons != 1 || len(status.MonMap.Monitors) != 1 {
t.Fatalf("expected 1 monitor, got %+v", status.MonMap)
}
if status.OSDMap.NumOSDs != 3 || status.OSDMap.NumUp != 2 || status.OSDMap.NumIn != 1 {
t.Fatalf("unexpected OSD map: %+v", status.OSDMap)
}
if status.OSDMap.NumDown != 1 || status.OSDMap.NumOut != 2 {
t.Fatalf("expected computed down/out counts, got %+v", status.OSDMap)
}
if status.PGMap.UsagePercent != 25.0 {
t.Fatalf("expected usage percent 25.0, got %v", status.PGMap.UsagePercent)
}
if len(status.Services) != 3 {
t.Fatalf("expected 3 service summaries, got %d", len(status.Services))
}
}
func TestParseStatus_CountOnlyFallbacks(t *testing.T) {
data := []byte(`{
"fsid":"fsid-counts",
"health":{"status":"HEALTH_OK","checks":{}},
"monmap":{"epoch":9,"num_mons":3,"quorum_names":["mon-a","mon-b","mon-c"]},
"mgrmap":{"available":true,"active_name":"mgr-a","num_standbys":1},
"osdmap":{"epoch":4,"num_osds":6,"num_up_osds":6,"num_in_osds":6},
"pgmap":{"num_pgs":128,"bytes_total":1000,"bytes_used":100,"bytes_avail":900}
}`)
status, err := parseStatus(data)
if err != nil {
t.Fatalf("parseStatus returned error: %v", err)
}
if status.MonMap.NumMons != 3 {
t.Fatalf("NumMons = %d, want 3", status.MonMap.NumMons)
}
if len(status.MonMap.Monitors) != 3 {
t.Fatalf("len(Monitors) = %d, want 3", len(status.MonMap.Monitors))
}
if status.MgrMap.NumMgrs != 2 {
t.Fatalf("NumMgrs = %d, want 2", status.MgrMap.NumMgrs)
}
if status.MgrMap.Standbys != 1 {
t.Fatalf("Standbys = %d, want 1", status.MgrMap.Standbys)
}
var monSvc, mgrSvc *ServiceInfo
for i := range status.Services {
switch status.Services[i].Type {
case "mon":
monSvc = &status.Services[i]
case "mgr":
mgrSvc = &status.Services[i]
}
}
if monSvc == nil || monSvc.Total != 3 {
t.Fatalf("mon service = %+v, want total 3", monSvc)
}
if mgrSvc == nil || mgrSvc.Total != 2 {
t.Fatalf("mgr service = %+v, want total 2", mgrSvc)
}
}
func TestParseStatus_ServiceMapFallbacks(t *testing.T) {
data := []byte(`{
"fsid":"fsid-servicemap",
"health":{"status":"HEALTH_OK","checks":{}},
"monmap":{"epoch":1},
"mgrmap":{"available":true},
"servicemap":{
"services":{
"mon":{"daemons":{
"a":{"status":"running","hostname":"node1"},
"b":{"status":"running","hostname":"node2"},
"c":{"status":"stopped","hostname":"node3"}
}},
"mgr":{"daemons":{
"mgr-a":{"status":"active","hostname":"node1"},
"mgr-b":{"status":"standby","hostname":"node2"}
}}
}
},
"osdmap":{"epoch":3,"num_osds":3,"num_up_osds":3,"num_in_osds":3},
"pgmap":{"num_pgs":64,"bytes_total":1000,"bytes_used":100,"bytes_avail":900}
}`)
status, err := parseStatus(data)
if err != nil {
t.Fatalf("parseStatus returned error: %v", err)
}
if status.MonMap.NumMons != 3 {
t.Fatalf("NumMons = %d, want 3", status.MonMap.NumMons)
}
if status.MgrMap.NumMgrs != 2 {
t.Fatalf("NumMgrs = %d, want 2", status.MgrMap.NumMgrs)
}
var monSvc, mgrSvc *ServiceInfo
for i := range status.Services {
switch status.Services[i].Type {
case "mon":
monSvc = &status.Services[i]
case "mgr":
mgrSvc = &status.Services[i]
}
}
if monSvc == nil || monSvc.Total != 3 || monSvc.Running != 2 {
t.Fatalf("mon service = %+v, want total 3 running 2", monSvc)
}
if mgrSvc == nil || mgrSvc.Total != 2 || mgrSvc.Running != 1 {
t.Fatalf("mgr service = %+v, want total 2 running 1", mgrSvc)
}
}
func TestIsAvailable(t *testing.T) {
t.Run("available", func(t *testing.T) {
withCommandRunner(t, func(ctx context.Context, name string, args ...string) ([]byte, []byte, error) {
if name != "which" || len(args) != 1 || args[0] != "ceph" {
t.Fatalf("unexpected command: %s %v", name, args)
}
return nil, nil, nil
})
if !IsAvailable(context.Background()) {
t.Fatalf("expected available")
}
})
t.Run("missing", func(t *testing.T) {
withCommandRunner(t, func(ctx context.Context, name string, args ...string) ([]byte, []byte, error) {
return nil, nil, errors.New("missing")
})
if IsAvailable(context.Background()) {
t.Fatalf("expected unavailable")
}
})
}
func TestRunCephCommand(t *testing.T) {
withCommandRunner(t, func(ctx context.Context, name string, args ...string) ([]byte, []byte, error) {
if name != "ceph" {
t.Fatalf("unexpected command name: %s", name)
}
if len(args) < 1 || args[0] != "status" {
t.Fatalf("unexpected args: %v", args)
}
return []byte(`{"ok":true}`), nil, nil
})
out, err := runCephCommand(context.Background(), "status", "--format", "json")
if err != nil {
t.Fatalf("runCephCommand error: %v", err)
}
if string(out) != `{"ok":true}` {
t.Fatalf("unexpected output: %s", string(out))
}
}
func TestRunCephCommandError(t *testing.T) {
withCommandRunner(t, func(ctx context.Context, name string, args ...string) ([]byte, []byte, error) {
return nil, []byte("bad"), errors.New("boom")
})
_, err := runCephCommand(context.Background(), "status", "--format", "json")
if err == nil {
t.Fatalf("expected error")
}
if !strings.Contains(err.Error(), "ceph status --format json failed") {
t.Fatalf("unexpected error message: %v", err)
}
if !strings.Contains(err.Error(), "stderr: bad") {
t.Fatalf("expected stderr in error: %v", err)
}
}
func TestParseStatusInvalidJSON(t *testing.T) {
_, err := parseStatus([]byte(`{not-json}`))
if err == nil {
t.Fatalf("expected error for invalid JSON")
}
}
func TestParseDF(t *testing.T) {
data := []byte(`{
"stats":{"total_bytes":1000,"total_used_bytes":123,"percent_used":0.1234},
"pools":[
{"id":1,"name":"pool-a","stats":{"bytes_used":10,"max_avail":90,"objects":7,"percent_used":0.2}}
]
}`)
pools, usagePercent, err := parseDF(data)
if err != nil {
t.Fatalf("parseDF returned error: %v", err)
}
if usagePercent != 12.34 {
t.Fatalf("expected percent_used 12.34, got %v", usagePercent)
}
if len(pools) != 1 || pools[0].Name != "pool-a" || pools[0].PercentUsed != 20.0 {
t.Fatalf("unexpected pools parsed: %+v", pools)
}
}
func TestParseDFInvalidJSON(t *testing.T) {
_, _, err := parseDF([]byte(`{not-json}`))
if err == nil {
t.Fatalf("expected error for invalid JSON")
}
}
func TestCollect_NotAvailable(t *testing.T) {
withCommandRunner(t, func(ctx context.Context, name string, args ...string) ([]byte, []byte, error) {
if name == "which" {
return nil, nil, errors.New("missing")
}
t.Fatalf("unexpected command: %s %v", name, args)
return nil, nil, nil
})
status, err := Collect(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if status != nil {
t.Fatalf("expected nil status")
}
}
func TestCollect_StatusError(t *testing.T) {
withCommandRunner(t, func(ctx context.Context, name string, args ...string) ([]byte, []byte, error) {
if name == "which" {
return nil, nil, nil
}
if name == "ceph" && len(args) > 0 && args[0] == "status" {
return nil, []byte("boom"), errors.New("status failed")
}
t.Fatalf("unexpected command: %s %v", name, args)
return nil, nil, nil
})
status, err := Collect(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if status != nil {
t.Fatalf("expected nil status")
}
}
func TestCollect_ParseStatusError(t *testing.T) {
withCommandRunner(t, func(ctx context.Context, name string, args ...string) ([]byte, []byte, error) {
if name == "which" {
return nil, nil, nil
}
if name == "ceph" && len(args) > 0 && args[0] == "status" {
return []byte(`{not-json}`), nil, nil
}
t.Fatalf("unexpected command: %s %v", name, args)
return nil, nil, nil
})
_, err := Collect(context.Background())
if err == nil {
t.Fatalf("expected parse error")
}
}
func TestCollect_DFCommandError(t *testing.T) {
statusJSON := []byte(`{
"fsid":"fsid-1",
"health":{"status":"HEALTH_OK","checks":{}},
"monmap":{"epoch":1,"mons":[]},
"mgrmap":{"available":false,"active_name":"","standbys":[]},
"osdmap":{"epoch":1,"num_osds":0,"num_up_osds":0,"num_in_osds":0},
"pgmap":{"num_pgs":0,"bytes_total":100,"bytes_used":50,"bytes_avail":50,
"data_bytes":0,"degraded_ratio":0,"misplaced_ratio":0,
"read_bytes_sec":0,"write_bytes_sec":0,"read_op_per_sec":0,"write_op_per_sec":0}
}`)
withCommandRunner(t, func(ctx context.Context, name string, args ...string) ([]byte, []byte, error) {
if name == "which" {
return nil, nil, nil
}
if name == "ceph" && len(args) > 0 && args[0] == "status" {
return statusJSON, nil, nil
}
if name == "ceph" && len(args) > 0 && args[0] == "df" {
return nil, []byte("df failed"), errors.New("df error")
}
t.Fatalf("unexpected command: %s %v", name, args)
return nil, nil, nil
})
status, err := Collect(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if status == nil {
t.Fatalf("expected status")
}
if status.PGMap.UsagePercent == 0 {
t.Fatalf("expected usage percent from status")
}
}
func TestCollect_DFParseError(t *testing.T) {
statusJSON := []byte(`{
"fsid":"fsid-1",
"health":{"status":"HEALTH_OK","checks":{}},
"monmap":{"epoch":1,"mons":[]},
"mgrmap":{"available":false,"active_name":"","standbys":[]},
"osdmap":{"epoch":1,"num_osds":0,"num_up_osds":0,"num_in_osds":0},
"pgmap":{"num_pgs":0,"bytes_total":0,"bytes_used":0,"bytes_avail":0,
"data_bytes":0,"degraded_ratio":0,"misplaced_ratio":0,
"read_bytes_sec":0,"write_bytes_sec":0,"read_op_per_sec":0,"write_op_per_sec":0}
}`)
withCommandRunner(t, func(ctx context.Context, name string, args ...string) ([]byte, []byte, error) {
if name == "which" {
return nil, nil, nil
}
if name == "ceph" && len(args) > 0 && args[0] == "status" {
return statusJSON, nil, nil
}
if name == "ceph" && len(args) > 0 && args[0] == "df" {
return []byte(`{not-json}`), nil, nil
}
t.Fatalf("unexpected command: %s %v", name, args)
return nil, nil, nil
})
status, err := Collect(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if status == nil {
t.Fatalf("expected status")
}
if status.PGMap.UsagePercent != 0 {
t.Fatalf("expected usage percent to remain 0, got %v", status.PGMap.UsagePercent)
}
}
func TestCollect_UsagePercentFromDF(t *testing.T) {
statusJSON := []byte(`{
"fsid":"fsid-1",
"health":{"status":"HEALTH_OK","checks":{}},
"monmap":{"epoch":1,"mons":[]},
"mgrmap":{"available":false,"active_name":"","standbys":[]},
"osdmap":{"epoch":1,"num_osds":0,"num_up_osds":0,"num_in_osds":0},
"pgmap":{"num_pgs":0,"bytes_total":0,"bytes_used":0,"bytes_avail":0,
"data_bytes":0,"degraded_ratio":0,"misplaced_ratio":0,
"read_bytes_sec":0,"write_bytes_sec":0,"read_op_per_sec":0,"write_op_per_sec":0}
}`)
dfJSON := []byte(`{
"stats":{"total_bytes":1000,"total_used_bytes":500,"percent_used":0.5},
"pools":[]
}`)
withCommandRunner(t, func(ctx context.Context, name string, args ...string) ([]byte, []byte, error) {
if name == "which" {
return nil, nil, nil
}
if name == "ceph" && len(args) > 0 && args[0] == "status" {
return statusJSON, nil, nil
}
if name == "ceph" && len(args) > 0 && args[0] == "df" {
return dfJSON, nil, nil
}
t.Fatalf("unexpected command: %s %v", name, args)
return nil, nil, nil
})
status, err := Collect(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if status == nil {
t.Fatalf("expected status")
}
if status.PGMap.UsagePercent != 50 {
t.Fatalf("expected usage percent from df, got %v", status.PGMap.UsagePercent)
}
if status.CollectedAt.IsZero() {
t.Fatalf("expected collected timestamp set")
}
}
func TestBoolToInt(t *testing.T) {
if boolToInt(true) != 1 {
t.Fatalf("expected boolToInt(true)=1")
}
if boolToInt(false) != 0 {
t.Fatalf("expected boolToInt(false)=0")
}
}