Every go-libvirt call runs under its own budget and a cancelled or overlong call kills the connection; credentials are read on every dial; one span and one log line per RPC come from a decorator applied where connections are made. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
264 lines
11 KiB
Go
264 lines
11 KiB
Go
package kvm2
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"time"
|
|
|
|
"github.com/digitalocean/go-libvirt"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/codes"
|
|
"go.opentelemetry.io/otel/trace"
|
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
|
|
|
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/tracing"
|
|
)
|
|
|
|
// observed is the span-decorator of spec entries 17 and 20: one child span
|
|
// kvm2.<Method> and one V(1) "libvirt call" line per call, with request and
|
|
// response detail at V(2). Applied once, in the dialer, over every backend
|
|
// that leaves it.
|
|
type observed struct {
|
|
inner backend
|
|
provider string
|
|
host string
|
|
tracer trace.Tracer
|
|
}
|
|
|
|
var (
|
|
_ backend = (*observed)(nil)
|
|
_ liveness = (*observed)(nil)
|
|
)
|
|
|
|
func observe(inner backend, providerName, hostName string, opts ...tracing.Option) backend {
|
|
return &observed{inner: inner, provider: providerName, host: hostName, tracer: tracing.Tracer(opts...)}
|
|
}
|
|
|
|
// dead forwards the inner backend's liveness; a backend without one is
|
|
// never dropped by the host table.
|
|
func (o *observed) dead() <-chan struct{} {
|
|
if l, ok := o.inner.(liveness); ok {
|
|
return l.dead()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// id is one identifier a call names: a domain, pool or volume. It becomes
|
|
// both the span attribute kvm2.<key> and the log key.
|
|
type id struct{ key, val string }
|
|
|
|
func domainID(name string) id { return id{"domain", name} }
|
|
func poolID(name string) id { return id{"pool", name} }
|
|
func volumeID(name string) id { return id{"volume", name} }
|
|
|
|
// detail returns V(2)-only key/value pairs. It runs only when V(2) is on,
|
|
// so building the list of a hundred domain names costs nothing otherwise.
|
|
type detail func() []any
|
|
|
|
// call wraps one backend call: span, timing, status and the log line. fn
|
|
// returns the V(2) detail for the line, or nil.
|
|
func (o *observed) call(ctx context.Context, rpc string, ids []id, fn func(ctx context.Context) (detail, error)) error {
|
|
attrs := make([]attribute.KeyValue, 0, len(ids)+1)
|
|
attrs = append(attrs, attribute.String("kvm2.host", o.host))
|
|
for _, i := range ids {
|
|
attrs = append(attrs, attribute.String("kvm2."+i.key, i.val))
|
|
}
|
|
ctx, span := tracing.StartSpan(ctx, o.tracer, "kvm2."+rpc, trace.WithSpanKind(trace.SpanKindClient), trace.WithAttributes(attrs...))
|
|
defer span.End()
|
|
log := logf.FromContext(ctx).WithName("kvm2").WithValues("provider", o.provider, "host", o.host)
|
|
start := time.Now()
|
|
more, err := fn(ctx)
|
|
kv := make([]any, 0, 8+2*len(ids))
|
|
kv = append(kv, "rpc", rpc)
|
|
for _, i := range ids {
|
|
kv = append(kv, i.key, i.val)
|
|
}
|
|
kv = append(kv, "duration", time.Since(start))
|
|
if err != nil {
|
|
span.SetStatus(codes.Error, err.Error())
|
|
if code, ok := libvirtCode(err); ok {
|
|
span.SetAttributes(attribute.Int("kvm2.libvirt_code", int(code)))
|
|
}
|
|
kv = append(kv, "error", err.Error())
|
|
kv = append(kv, libvirtErrorFields(err)...)
|
|
}
|
|
if more != nil && log.V(2).Enabled() {
|
|
kv = append(kv, more()...)
|
|
}
|
|
log.V(1).Info("libvirt call", kv...)
|
|
return err
|
|
}
|
|
|
|
func (o *observed) DomainLookupByName(ctx context.Context, name string) (dom libvirt.Domain, err error) {
|
|
err = o.call(ctx, "DomainLookupByName", []id{domainID(name)}, func(ctx context.Context) (detail, error) {
|
|
dom, err = o.inner.DomainLookupByName(ctx, name)
|
|
return nil, err
|
|
})
|
|
return dom, err
|
|
}
|
|
|
|
func (o *observed) DomainDefineXML(ctx context.Context, xml string) (dom libvirt.Domain, err error) {
|
|
err = o.call(ctx, "DomainDefineXML", nil, func(ctx context.Context) (detail, error) {
|
|
dom, err = o.inner.DomainDefineXML(ctx, xml)
|
|
return func() []any { return []any{"domain", dom.Name, "xml", xml} }, err
|
|
})
|
|
return dom, err
|
|
}
|
|
|
|
func (o *observed) DomainCreate(ctx context.Context, dom libvirt.Domain) error {
|
|
return o.call(ctx, "DomainCreate", []id{domainID(dom.Name)}, func(ctx context.Context) (detail, error) {
|
|
return nil, o.inner.DomainCreate(ctx, dom)
|
|
})
|
|
}
|
|
|
|
func (o *observed) DomainDestroy(ctx context.Context, dom libvirt.Domain) error {
|
|
return o.call(ctx, "DomainDestroy", []id{domainID(dom.Name)}, func(ctx context.Context) (detail, error) {
|
|
return nil, o.inner.DomainDestroy(ctx, dom)
|
|
})
|
|
}
|
|
|
|
func (o *observed) DomainUndefineFlags(ctx context.Context, dom libvirt.Domain, flags libvirt.DomainUndefineFlagsValues) error {
|
|
return o.call(ctx, "DomainUndefineFlags", []id{domainID(dom.Name)}, func(ctx context.Context) (detail, error) {
|
|
return nil, o.inner.DomainUndefineFlags(ctx, dom, flags)
|
|
})
|
|
}
|
|
|
|
func (o *observed) DomainGetState(ctx context.Context, dom libvirt.Domain, flags uint32) (state int32, reason int32, err error) {
|
|
err = o.call(ctx, "DomainGetState", []id{domainID(dom.Name)}, func(ctx context.Context) (detail, error) {
|
|
state, reason, err = o.inner.DomainGetState(ctx, dom, flags)
|
|
return func() []any { return []any{"state", libvirt.DomainState(state).String(), "reason", reason} }, err
|
|
})
|
|
return state, reason, err
|
|
}
|
|
|
|
func (o *observed) DomainGetMetadata(ctx context.Context, dom libvirt.Domain, typ int32, uri libvirt.OptString, flags libvirt.DomainModificationImpact) (md string, err error) {
|
|
err = o.call(ctx, "DomainGetMetadata", []id{domainID(dom.Name)}, func(ctx context.Context) (detail, error) {
|
|
md, err = o.inner.DomainGetMetadata(ctx, dom, typ, uri, flags)
|
|
return func() []any { return []any{"metadata", md} }, err
|
|
})
|
|
return md, err
|
|
}
|
|
|
|
func (o *observed) DomainInterfaceAddresses(ctx context.Context, dom libvirt.Domain, source uint32, flags uint32) (ifaces []libvirt.DomainInterface, err error) {
|
|
err = o.call(ctx, "DomainInterfaceAddresses", []id{domainID(dom.Name)}, func(ctx context.Context) (detail, error) {
|
|
ifaces, err = o.inner.DomainInterfaceAddresses(ctx, dom, source, flags)
|
|
return func() []any { return []any{"source", source, "addresses", interfaceAddresses(ifaces)} }, err
|
|
})
|
|
return ifaces, err
|
|
}
|
|
|
|
func (o *observed) ConnectListAllDomains(ctx context.Context, needResults int32, flags libvirt.ConnectListAllDomainsFlags) (doms []libvirt.Domain, n uint32, err error) {
|
|
err = o.call(ctx, "ConnectListAllDomains", nil, func(ctx context.Context) (detail, error) {
|
|
doms, n, err = o.inner.ConnectListAllDomains(ctx, needResults, flags)
|
|
return func() []any { return []any{"count", len(doms), "domains", domainNames(doms)} }, err
|
|
})
|
|
return doms, n, err
|
|
}
|
|
|
|
func (o *observed) StoragePoolLookupByName(ctx context.Context, name string) (pool libvirt.StoragePool, err error) {
|
|
err = o.call(ctx, "StoragePoolLookupByName", []id{poolID(name)}, func(ctx context.Context) (detail, error) {
|
|
pool, err = o.inner.StoragePoolLookupByName(ctx, name)
|
|
return nil, err
|
|
})
|
|
return pool, err
|
|
}
|
|
|
|
func (o *observed) StoragePoolRefresh(ctx context.Context, pool libvirt.StoragePool, flags uint32) error {
|
|
return o.call(ctx, "StoragePoolRefresh", []id{poolID(pool.Name)}, func(ctx context.Context) (detail, error) {
|
|
return nil, o.inner.StoragePoolRefresh(ctx, pool, flags)
|
|
})
|
|
}
|
|
|
|
func (o *observed) StoragePoolGetInfo(ctx context.Context, pool libvirt.StoragePool) (state uint8, capacity, allocation, available uint64, err error) {
|
|
err = o.call(ctx, "StoragePoolGetInfo", []id{poolID(pool.Name)}, func(ctx context.Context) (detail, error) {
|
|
state, capacity, allocation, available, err = o.inner.StoragePoolGetInfo(ctx, pool)
|
|
return func() []any {
|
|
return []any{"state", libvirt.StoragePoolState(state).String(), "capacity", capacity, "allocation", allocation, "available", available}
|
|
}, err
|
|
})
|
|
return state, capacity, allocation, available, err
|
|
}
|
|
|
|
func (o *observed) StorageVolLookupByName(ctx context.Context, pool libvirt.StoragePool, name string) (vol libvirt.StorageVol, err error) {
|
|
err = o.call(ctx, "StorageVolLookupByName", []id{poolID(pool.Name), volumeID(name)}, func(ctx context.Context) (detail, error) {
|
|
vol, err = o.inner.StorageVolLookupByName(ctx, pool, name)
|
|
return nil, err
|
|
})
|
|
return vol, err
|
|
}
|
|
|
|
func (o *observed) StorageVolGetPath(ctx context.Context, vol libvirt.StorageVol) (path string, err error) {
|
|
err = o.call(ctx, "StorageVolGetPath", []id{poolID(vol.Pool), volumeID(vol.Name)}, func(ctx context.Context) (detail, error) {
|
|
path, err = o.inner.StorageVolGetPath(ctx, vol)
|
|
return func() []any { return []any{"path", path} }, err
|
|
})
|
|
return path, err
|
|
}
|
|
|
|
func (o *observed) StorageVolCreateXML(ctx context.Context, pool libvirt.StoragePool, xml string, flags libvirt.StorageVolCreateFlags) (vol libvirt.StorageVol, err error) {
|
|
err = o.call(ctx, "StorageVolCreateXML", []id{poolID(pool.Name)}, func(ctx context.Context) (detail, error) {
|
|
vol, err = o.inner.StorageVolCreateXML(ctx, pool, xml, flags)
|
|
return func() []any { return []any{"volume", vol.Name, "xml", xml} }, err
|
|
})
|
|
return vol, err
|
|
}
|
|
|
|
// StorageVolUpload and StorageVolDownload log the length only; the bodies
|
|
// are a seed ISO carrying CloudInit or a base image.
|
|
func (o *observed) StorageVolUpload(ctx context.Context, vol libvirt.StorageVol, r io.Reader, offset, length uint64, flags libvirt.StorageVolUploadFlags) error {
|
|
return o.call(ctx, "StorageVolUpload", []id{poolID(vol.Pool), volumeID(vol.Name)}, func(ctx context.Context) (detail, error) {
|
|
err := o.inner.StorageVolUpload(ctx, vol, r, offset, length, flags)
|
|
return func() []any { return []any{"offset", offset, "length", length} }, err
|
|
})
|
|
}
|
|
|
|
func (o *observed) StorageVolDownload(ctx context.Context, vol libvirt.StorageVol, w io.Writer, offset, length uint64, flags libvirt.StorageVolDownloadFlags) error {
|
|
return o.call(ctx, "StorageVolDownload", []id{poolID(vol.Pool), volumeID(vol.Name)}, func(ctx context.Context) (detail, error) {
|
|
err := o.inner.StorageVolDownload(ctx, vol, w, offset, length, flags)
|
|
return func() []any { return []any{"offset", offset, "length", length} }, err
|
|
})
|
|
}
|
|
|
|
func (o *observed) StorageVolDelete(ctx context.Context, vol libvirt.StorageVol, flags libvirt.StorageVolDeleteFlags) error {
|
|
return o.call(ctx, "StorageVolDelete", []id{poolID(vol.Pool), volumeID(vol.Name)}, func(ctx context.Context) (detail, error) {
|
|
return nil, o.inner.StorageVolDelete(ctx, vol, flags)
|
|
})
|
|
}
|
|
|
|
func (o *observed) ConnectGetLibVersion(ctx context.Context) (v uint64, err error) {
|
|
err = o.call(ctx, "ConnectGetLibVersion", nil, func(ctx context.Context) (detail, error) {
|
|
v, err = o.inner.ConnectGetLibVersion(ctx)
|
|
return func() []any { return []any{"libvirtVersion", v} }, err
|
|
})
|
|
return v, err
|
|
}
|
|
|
|
func (o *observed) ConnectGetHostname(ctx context.Context) (name string, err error) {
|
|
err = o.call(ctx, "ConnectGetHostname", nil, func(ctx context.Context) (detail, error) {
|
|
name, err = o.inner.ConnectGetHostname(ctx)
|
|
return func() []any { return []any{"hostname", name} }, err
|
|
})
|
|
return name, err
|
|
}
|
|
|
|
// Close is passed through: it is not a libvirt call and gets no span.
|
|
func (o *observed) Close() error { return o.inner.Close() }
|
|
|
|
func domainNames(doms []libvirt.Domain) []string {
|
|
names := make([]string, len(doms))
|
|
for i, d := range doms {
|
|
names[i] = d.Name
|
|
}
|
|
return names
|
|
}
|
|
|
|
func interfaceAddresses(ifaces []libvirt.DomainInterface) []string {
|
|
var out []string
|
|
for _, iface := range ifaces {
|
|
for _, a := range iface.Addrs {
|
|
out = append(out, iface.Name+"="+a.Addr)
|
|
}
|
|
}
|
|
return out
|
|
}
|