Files
kacerrandClaude Opus 5 04dd2c0284 Add the KVM health check: reachability and storage pool per host
Each configured host is checked concurrently over the pooled SSH+libvirt
connection: libvirt version and hostname prove the daemon answers, then
the configured pool must exist and be running, with its free space
reported. Dial failures are now tagged with their stage (SSH material,
TCP, SSH handshake, libvirt connect) by a dialError that keeps the
message and unwraps to the cause, so classification is unchanged and the
check can say why a host is unreachable. A pool verdict is decided after
the call returns so it never drops a working connection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 20:17:35 +00:00

233 lines
8.0 KiB
Go

package kvm
import (
"fmt"
"io"
"time"
libvirt "github.com/digitalocean/go-libvirt"
"github.com/go-logr/logr"
)
// tracingAPI wraps a libvirtAPI and logs one V(1) line per call after it
// returns: the libvirt API function (rpc=virDomainLookupByName, ...), the
// identifiers it was given, curated results, duration, and on failure the
// error with libvirt's code name. At V(2) the same line also carries XML
// bodies, metadata and address lists. Upload and download bodies are never
// logged: the seed ISO carries cloud-init.
type tracingAPI struct {
inner libvirtAPI
log logr.Logger
}
type tracedCall struct {
t *tracingAPI
method string
start time.Time
kv []any
}
// begin names the call by its libvirt API function: go-libvirt's method
// names are the remote procedure names, which are the public vir* API
// names without the prefix.
func (t *tracingAPI) begin(method string, kv ...any) *tracedCall {
return &tracedCall{t: t, method: "vir" + method, start: time.Now(), kv: kv}
}
// detail adds key/values that are only worth the bytes at V(2).
func (c *tracedCall) detail(kv ...any) *tracedCall {
if c.t.log.V(2).Enabled() {
c.kv = append(c.kv, kv...)
}
return c
}
func (c *tracedCall) done(err error, kv ...any) {
fields := make([]any, 0, 2+len(c.kv)+len(kv)+10)
fields = append(fields, "rpc", c.method)
fields = append(fields, c.kv...)
fields = append(fields, kv...)
fields = append(fields, "duration", time.Since(c.start).Round(time.Microsecond))
if err != nil {
fields = append(fields, "error", err.Error())
fields = append(fields, libvirtErrorFields(err)...)
}
c.t.log.V(1).Info("libvirt call", fields...)
}
// flagKV renders a flags value only when something is set; go-libvirt's
// generated stringers do not know bitmask combinations.
func flagKV(v int64) []any {
if v == 0 {
return nil
}
return []any{"flags", v}
}
func optString(s libvirt.OptString) string {
if len(s) == 0 {
return ""
}
return s[0]
}
func (t *tracingAPI) DomainLookupByName(name string) (libvirt.Domain, error) {
c := t.begin("DomainLookupByName", "domain", name)
d, err := t.inner.DomainLookupByName(name)
c.done(err)
return d, err
}
func (t *tracingAPI) DomainDefineXML(xml string) (libvirt.Domain, error) {
c := t.begin("DomainDefineXML", "xmlBytes", len(xml)).detail("xml", xml)
d, err := t.inner.DomainDefineXML(xml)
c.done(err, "domain", d.Name)
return d, err
}
func (t *tracingAPI) DomainCreate(d libvirt.Domain) error {
c := t.begin("DomainCreate", "domain", d.Name)
err := t.inner.DomainCreate(d)
c.done(err)
return err
}
func (t *tracingAPI) DomainDestroy(d libvirt.Domain) error {
c := t.begin("DomainDestroy", "domain", d.Name)
err := t.inner.DomainDestroy(d)
c.done(err)
return err
}
func (t *tracingAPI) DomainUndefineFlags(d libvirt.Domain, flags libvirt.DomainUndefineFlagsValues) error {
c := t.begin("DomainUndefineFlags", append([]any{"domain", d.Name}, flagKV(int64(flags))...)...)
err := t.inner.DomainUndefineFlags(d, flags)
c.done(err)
return err
}
func (t *tracingAPI) DomainGetState(d libvirt.Domain, flags uint32) (int32, int32, error) {
c := t.begin("DomainGetState", "domain", d.Name)
state, reason, err := t.inner.DomainGetState(d, flags)
c.done(err, "state", libvirt.DomainState(state), "reason", reason)
return state, reason, err
}
func (t *tracingAPI) DomainGetMetadata(d libvirt.Domain, typ int32, uri libvirt.OptString, flags libvirt.DomainModificationImpact) (string, error) {
c := t.begin("DomainGetMetadata", "domain", d.Name, "type", typ, "uri", optString(uri))
s, err := t.inner.DomainGetMetadata(d, typ, uri, flags)
c.detail("metadata", s).done(err, "metadataBytes", len(s))
return s, err
}
func (t *tracingAPI) DomainInterfaceAddresses(d libvirt.Domain, source uint32, flags uint32) ([]libvirt.DomainInterface, error) {
c := t.begin("DomainInterfaceAddresses", "domain", d.Name, "source", libvirt.DomainInterfaceAddressesSource(source))
ifaces, err := t.inner.DomainInterfaceAddresses(d, source, flags)
if t.log.V(2).Enabled() {
c.detail("addresses", formatAddresses(ifaces))
}
c.done(err, "interfaces", len(ifaces))
return ifaces, err
}
func formatAddresses(ifaces []libvirt.DomainInterface) []string {
var out []string
for _, iface := range ifaces {
for _, a := range iface.Addrs {
out = append(out, fmt.Sprintf("%s=%s/%d", iface.Name, a.Addr, a.Prefix))
}
}
return out
}
func (t *tracingAPI) ConnectListAllDomains(needResults int32, flags libvirt.ConnectListAllDomainsFlags) ([]libvirt.Domain, uint32, error) {
c := t.begin("ConnectListAllDomains", flagKV(int64(flags))...)
doms, n, err := t.inner.ConnectListAllDomains(needResults, flags)
if t.log.V(2).Enabled() {
names := make([]string, 0, len(doms))
for _, d := range doms {
names = append(names, d.Name)
}
c.detail("names", names)
}
c.done(err, "count", len(doms))
return doms, n, err
}
func (t *tracingAPI) StoragePoolLookupByName(name string) (libvirt.StoragePool, error) {
c := t.begin("StoragePoolLookupByName", "pool", name)
pool, err := t.inner.StoragePoolLookupByName(name)
c.done(err)
return pool, err
}
func (t *tracingAPI) StoragePoolRefresh(pool libvirt.StoragePool, flags uint32) error {
c := t.begin("StoragePoolRefresh", "pool", pool.Name)
err := t.inner.StoragePoolRefresh(pool, flags)
c.done(err)
return err
}
func (t *tracingAPI) StorageVolLookupByName(pool libvirt.StoragePool, name string) (libvirt.StorageVol, error) {
c := t.begin("StorageVolLookupByName", "pool", pool.Name, "volume", name)
vol, err := t.inner.StorageVolLookupByName(pool, name)
c.done(err)
return vol, err
}
func (t *tracingAPI) StorageVolGetPath(vol libvirt.StorageVol) (string, error) {
c := t.begin("StorageVolGetPath", "pool", vol.Pool, "volume", vol.Name)
path, err := t.inner.StorageVolGetPath(vol)
c.done(err, "path", path)
return path, err
}
func (t *tracingAPI) StorageVolCreateXML(pool libvirt.StoragePool, xml string, flags libvirt.StorageVolCreateFlags) (libvirt.StorageVol, error) {
c := t.begin("StorageVolCreateXML", append([]any{"pool", pool.Name, "xmlBytes", len(xml)}, flagKV(int64(flags))...)...).detail("xml", xml)
vol, err := t.inner.StorageVolCreateXML(pool, xml, flags)
c.done(err, "volume", vol.Name)
return vol, err
}
func (t *tracingAPI) StorageVolUpload(vol libvirt.StorageVol, r io.Reader, offset, length uint64, flags libvirt.StorageVolUploadFlags) error {
c := t.begin("StorageVolUpload", append([]any{"pool", vol.Pool, "volume", vol.Name, "offset", offset, "length", length}, flagKV(int64(flags))...)...)
err := t.inner.StorageVolUpload(vol, r, offset, length, flags)
c.done(err)
return err
}
func (t *tracingAPI) StorageVolDownload(vol libvirt.StorageVol, w io.Writer, offset, length uint64, flags libvirt.StorageVolDownloadFlags) error {
c := t.begin("StorageVolDownload", append([]any{"pool", vol.Pool, "volume", vol.Name, "offset", offset, "length", length}, flagKV(int64(flags))...)...)
err := t.inner.StorageVolDownload(vol, w, offset, length, flags)
c.done(err)
return err
}
func (t *tracingAPI) StorageVolDelete(vol libvirt.StorageVol, flags libvirt.StorageVolDeleteFlags) error {
c := t.begin("StorageVolDelete", append([]any{"pool", vol.Pool, "volume", vol.Name}, flagKV(int64(flags))...)...)
err := t.inner.StorageVolDelete(vol, flags)
c.done(err)
return err
}
func (t *tracingAPI) ConnectGetLibVersion() (uint64, error) {
c := t.begin("ConnectGetLibVersion")
v, err := t.inner.ConnectGetLibVersion()
c.done(err, "version", formatLibVersion(v))
return v, err
}
func (t *tracingAPI) ConnectGetHostname() (string, error) {
c := t.begin("ConnectGetHostname")
h, err := t.inner.ConnectGetHostname()
c.done(err, "hostname", h)
return h, err
}
func (t *tracingAPI) StoragePoolGetInfo(pool libvirt.StoragePool) (uint8, uint64, uint64, uint64, error) {
c := t.begin("StoragePoolGetInfo", "pool", pool.Name)
state, capacity, allocation, available, err := t.inner.StoragePoolGetInfo(pool)
c.done(err, "state", poolStateName(libvirt.StoragePoolState(state)), "capacity", capacity, "available", available)
return state, capacity, allocation, available, err
}