Files
gvm/issues_test.go
2026-09-08 15:50:23 +02:00

218 lines
7.5 KiB
Go

package main
import (
"strings"
"testing"
"time"
"github.com/vmware/govmomi/vim25/types"
)
// aged builds a snapshot entry that was taken so many days ago.
func aged(name string, days int) snapEntry {
when := time.Now().Add(-time.Duration(days) * 24 * time.Hour)
return snapEntry{
ref: types.ManagedObjectReference{Type: "VirtualMachineSnapshot", Value: "snapshot-" + name},
name: name,
when: when,
created: when.Local().Format("02.01.2006 15:04"),
}
}
// A machine with nothing wrong with it says nothing. This is the one that
// matters: the whole point of the filter is that it is short.
func TestAHealthyMachineHasNoIssues(t *testing.T) {
r := testRow("web01", true, "10.0.0.5")
if got := r.issues(); len(got) > 0 {
t.Errorf("a healthy machine reported %v", got)
}
if r.hasIssues() {
t.Error("a healthy machine is in the issues list")
}
if r.issueColor() != colOff {
t.Error("a healthy machine's reason is coloured as though it had one")
}
}
// A machine that is switched off is not a fault, and the things that are only
// true of a running machine are not held against a stopped one.
func TestAStoppedMachineIsNotAnIssue(t *testing.T) {
r := testRow("web01", false, "10.0.0.5")
r.vm.Guest.ToolsRunningStatus = "guestToolsNotRunning"
r.vm.Guest.Disk = []types.GuestDiskInfo{{DiskPath: "/", Capacity: 100, FreeSpace: 1}}
if got := r.issues(); len(got) > 0 {
t.Errorf("a stopped machine reported %v", got)
}
}
func TestIssuesFound(t *testing.T) {
for _, c := range []struct {
what string
bend func(*vmRow)
want string
bad bool
}{
{"disconnected", func(r *vmRow) {
r.vm.Summary.Runtime.ConnectionState = types.VirtualMachineConnectionStateDisconnected
}, "disconnected", true},
{"orphaned", func(r *vmRow) {
r.vm.Summary.Runtime.ConnectionState = types.VirtualMachineConnectionStateOrphaned
}, "orphaned", true},
{"a question", func(r *vmRow) {
r.vm.Summary.Runtime.Question = &types.VirtualMachineQuestionInfo{Id: "1"}
}, "waiting for an answer", true},
{"consolidation", func(r *vmRow) {
r.vm.Summary.Runtime.ConsolidationNeeded = true
}, "consolidating", true},
{"a red status", func(r *vmRow) {
r.vm.Summary.OverallStatus = types.ManagedEntityStatusRed
}, "vCenter says red", true},
{"a yellow status", func(r *vmRow) {
r.vm.Summary.OverallStatus = types.ManagedEntityStatusYellow
}, "vCenter says yellow", false},
{"no tools", func(r *vmRow) {
r.vm.Guest.ToolsRunningStatus = "guestToolsNotRunning"
r.vm.Summary.Guest = &types.VirtualMachineGuestSummary{ToolsRunningStatus: "guestToolsNotRunning"}
}, "no VMware Tools", false},
{"a full filesystem", func(r *vmRow) {
r.vm.Guest.Disk = []types.GuestDiskInfo{{DiskPath: "/var", Capacity: 100 << 30, FreeSpace: 3 << 30}}
}, "/var 97 % full", true},
{"a nearly full filesystem", func(r *vmRow) {
r.vm.Guest.Disk = []types.GuestDiskInfo{{DiskPath: "/boot", Capacity: 100 << 30, FreeSpace: 8 << 30}}
}, "/boot 92 % full", false},
{"an old snapshot", func(r *vmRow) {
r.snaps = []snapEntry{aged("before-patch", 63)}
}, "before-patch is 63 days old", false},
} {
r := testRow("web01", true, "10.0.0.5")
c.bend(&r)
list := r.issueList()
found := false
for _, i := range list {
if strings.Contains(i.text, c.want) {
found = true
if i.bad != c.bad {
t.Errorf("%s: bad = %v, want %v (%q)", c.what, i.bad, c.bad, i.text)
}
}
}
if !found {
t.Errorf("%s: nothing said %q, only %v", c.what, c.want, r.issues())
}
if !r.hasIssues() {
t.Errorf("%s: the machine is not in the issues list", c.what)
}
}
}
// A filesystem that is merely fairly full is nobody's business.
func TestAFilesystemWithRoomIsNotReported(t *testing.T) {
r := testRow("web01", true, "10.0.0.5")
r.vm.Guest.Disk = []types.GuestDiskInfo{{DiskPath: "/", Capacity: 100 << 30, FreeSpace: 20 << 30}}
if got := r.issues(); len(got) > 0 {
t.Errorf("a filesystem at 80 %% reported %v", got)
}
}
// A snapshot is only old once it has stopped being somebody's afternoon. The
// table colours it yellow after a week; the list of things to answer for waits
// for a month, or it fills up with this morning's work.
func TestOnlyAMonthOldSnapshotIsAnIssue(t *testing.T) {
for _, c := range []struct {
days int
want bool
}{{2, false}, {snapStaleDays + 1, false}, {snapOldDays, true}, {90, true}} {
r := testRow("web01", true, "10.0.0.5")
r.snaps = []snapEntry{aged("s", c.days)}
if got := r.hasIssues(); got != c.want {
t.Errorf("a snapshot of %d days: reported = %v, want %v (%v)",
c.days, got, c.want, r.issues())
}
}
}
// vCenter's own alarms are passed on by the name somebody gave them, and an
// alarm that has been acknowledged has been dealt with by a person already.
func TestAlarms(t *testing.T) {
ref := types.ManagedObjectReference{Type: "Alarm", Value: "alarm-14"}
yes := true
r := testRow("web01", true, "10.0.0.5")
r.vm.TriggeredAlarmState = []types.AlarmState{
{Alarm: ref, OverallStatus: types.ManagedEntityStatusRed},
}
// Without the names, the reference is still said: it is little use, but it
// is not silence.
if got := strings.Join(r.issues(), " "); !strings.Contains(got, "alarm-14") {
t.Errorf("an alarm with no name resolved reported %q", got)
}
r.sess = &session{alarms: map[types.ManagedObjectReference]string{ref: "Host memory usage"}}
if got := strings.Join(r.issues(), " "); !strings.Contains(got, "Host memory usage") {
t.Errorf("the alarm's name was not used: %q", got)
}
r.vm.TriggeredAlarmState[0].Acknowledged = &yes
if got := r.issues(); len(got) > 0 {
t.Errorf("an acknowledged alarm still reported %v", got)
}
}
// The rolled-up status is not reported next to the alarm it is the rollup of:
// the same fact twice, once with a reason and once without.
func TestTheStatusIsNotReportedTwice(t *testing.T) {
r := testRow("web01", true, "10.0.0.5")
r.vm.Summary.OverallStatus = types.ManagedEntityStatusRed
r.vm.TriggeredAlarmState = []types.AlarmState{{
Alarm: types.ManagedObjectReference{Type: "Alarm", Value: "alarm-1"},
OverallStatus: types.ManagedEntityStatusRed,
}}
if got := r.issues(); len(got) != 1 {
t.Errorf("a red machine with one alarm reported %d things: %v", len(got), got)
}
if got := strings.Join(r.issues(), " "); strings.Contains(got, "says red") {
t.Errorf("the rollup was reported beside its own alarm: %q", got)
}
}
// Worst first, because the column they end up in is truncated from the right.
func TestTheWorstReasonComesFirst(t *testing.T) {
r := testRow("web01", true, "10.0.0.5")
r.vm.Guest.ToolsRunningStatus = "guestToolsNotRunning" // a word of warning
r.vm.Summary.Runtime.ConsolidationNeeded = true // broken
list := r.issueList()
if len(list) < 2 {
t.Fatalf("expected both reasons, got %v", r.issues())
}
if !list[0].bad {
t.Errorf("the reasons came out warning first: %v", r.issues())
}
if r.issueColor() != colFull {
t.Error("a machine with something broken is not painted as broken")
}
// Only a warning: yellow, not red.
r.vm.Summary.Runtime.ConsolidationNeeded = false
if r.issueColor() != colBusy {
t.Error("a machine with only a warning is painted as broken")
}
if cell := r.issueCell(); !strings.Contains(cell, "no VMware Tools") {
t.Errorf("the reason column says %q", cell)
}
}
func TestWithIssuesKeepsOnlyTheOnesToAnswerFor(t *testing.T) {
good := testRow("web01", true, "10.0.0.5")
bad := testRow("db01", true, "10.0.0.6")
bad.vm.Summary.Runtime.ConsolidationNeeded = true
got := withIssues([]vmRow{good, bad})
if len(got) != 1 || got[0].name != "db01" {
t.Errorf("the filter kept %d machines: %v", len(got), got)
}
}