Files
2026-09-06 14:11:46 +02:00

181 lines
5.0 KiB
Go

// host.go — what the ESXi hosts are doing.
package main
import (
"bytes"
"fmt"
"net/http"
"strings"
"time"
"github.com/vmware/govmomi/units"
"github.com/vmware/govmomi/vim25/types"
)
// hoststat prints one line per host: CPU load, memory, how many machines it
// carries and how many of them run.
//
// Every number here comes out of host.Summary, and vSphere leaves parts of that
// out for a host it cannot currently talk to. Reading it unguarded meant that a
// single host in maintenance took down the whole report — including the healthy
// hosts, which is the very thing one wants to see at that moment.
func hoststat(vc VCenter, telemetry string) error {
s, err := connect(vc)
if err != nil {
return err
}
defer s.close()
hosts, err := s.hosts("name", "vm", "overallStatus", "runtime.connectionState", "summary")
if err != nil {
return err
}
running, err := powerStates(s)
if err != nil {
return err
}
printRow(hostColumns, "", nil) // the heading, from the same widths as the rows
for _, host := range hosts {
hn := shortHost(host.Name)
total := len(host.Vm)
on := countOn(host.Vm, running)
usedMem := int64(host.Summary.QuickStats.OverallMemoryUsage) * 1024 * 1024
totalMem := int64(0)
cpu, cpuKnown := 0.0, false
if hw := host.Summary.Hardware; hw != nil {
totalMem = hw.MemorySize
cpu, cpuKnown = cpuPercent(hw, host.Summary.QuickStats)
}
cpuText, cpuCol := "-", colOff
if cpuKnown {
cpuText, cpuCol = SF("%.2f", cpu), loadColor(cpu, true)
}
printRow(hostColumns, "", []cell{
{hn, cWhite.fg()},
{cpuText, cpuCol},
{units.ByteSize(usedMem).String(), colSize},
{units.ByteSize(totalMem).String(), colSize},
{Itoa(total), colSize},
{Itoa(on), colSize},
{string(host.OverallStatus), statusColor(host.OverallStatus)},
{string(host.Runtime.ConnectionState), colAside},
})
if telemetry != "" {
post(telemetry, SF("vm,%s,%.2f,%d,%d,%d,%d,%s,%s",
hn, cpu, usedMem, totalMem, total, on,
host.OverallStatus, host.Runtime.ConnectionState))
}
}
return nil
}
// vmstat is the short form: how many machines each host carries, and how many of
// them are powered on.
func vmstat(vc VCenter) error {
s, err := connect(vc)
if err != nil {
return err
}
defer s.close()
hosts, err := s.hosts("name", "vm")
if err != nil {
return err
}
running, err := powerStates(s)
if err != nil {
return err
}
printRow(countColumns, "", nil)
for _, host := range hosts {
printRow(countColumns, "", []cell{
{host.Name, cWhite.fg()},
{Itoa(len(host.Vm)), colSize},
{Itoa(countOn(host.Vm, running)), colSize},
})
}
return nil
}
// powerStates maps every machine in the inventory to whether it is running, so
// the hosts can be counted without asking per machine.
func powerStates(s *session) (map[types.ManagedObjectReference]bool, error) {
vms, err := s.vms("runtime.powerState")
if err != nil {
return nil, err
}
on := make(map[types.ManagedObjectReference]bool, len(vms))
for _, vm := range vms {
on[vm.Reference()] = vm.Runtime.PowerState == types.VirtualMachinePowerStatePoweredOn
}
return on, nil
}
// countOn counts a host's running machines. Counting straight off host.Vm is
// what keeps the columns honest: the old version filled two maps keyed by
// host.Name and read them back keyed by host.Summary.Config.Name, two different
// vSphere properties, so a host added by address and renamed later reported 0
// machines while running dozens.
func countOn(refs []types.ManagedObjectReference, running map[types.ManagedObjectReference]bool) int {
n := 0
for _, ref := range refs {
if running[ref] {
n++
}
}
return n
}
// cpuPercent is the host's CPU load in percent. It reports false when the
// hardware summary has no usable clock or core count, rather than dividing by
// zero and printing "+Inf" in a column six characters wide.
func cpuPercent(hw *types.HostHardwareSummary, qs types.HostListSummaryQuickStats) (float64, bool) {
totalMHz := int64(hw.CpuMhz) * int64(hw.NumCpuCores)
if totalMHz <= 0 {
return 0, false
}
return 100.0 / float64(totalMHz) * float64(qs.OverallCpuUsage), true
}
// statusColor: vSphere's own words for how a host is doing, in the palette's.
func statusColor(st types.ManagedEntityStatus) string {
switch string(st) {
case "green":
return colOK
case "yellow":
return colBusy
case "red":
return colFull
}
return colAside
}
// shortHost is the hostname without its domain, which is all the first column
// has room for.
func shortHost(name string) string {
if i := strings.IndexByte(name, '.'); i > 0 {
return name[:i]
}
return name
}
// post hands one line to the monitoring server. A failure is worth a word but
// not the report: the numbers on screen are the point, the telemetry is a copy.
func post(url, data string) {
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Post(url, "application/data", bytes.NewReader([]byte(data)))
if err != nil {
PE("telemetry", err.Error())
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
PE("telemetry", fmt.Sprintf("%s said %s", url, resp.Status))
}
}