Files
egress-proxies-operator/internal/provider/kvm2/kvm2.go
T
kacerrandClaude Fable 5.1 b45f83327a kvm2: SSH transport, ctx-first libvirt seam, host table and span decorator
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>
2026-09-16 17:43:27 +00:00

148 lines
4.8 KiB
Go

// Package kvm2 implements the provider contract on libvirt/KVM hypervisors
// reached over SSH: the host's libvirt unix socket is forwarded through the
// SSH connection and driven with go-libvirt's RPC client. The design is in
// docs/kvm2-provider-spec.md.
package kvm2
import (
"context"
"errors"
"os"
"time"
"github.com/go-logr/logr"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
)
const (
defaultSocket = "/var/run/libvirt/libvirt-sock"
defaultPool = "default"
defaultDiskSizeGB = 10
)
// Provider is one configured kvm2 entry: a set of hosts sharing one SSH key
// and one image and machine-type catalogue.
type Provider struct {
name string
cfg provider.KVMConfig
now func() time.Time
hosts *hostTable
}
var _ provider.Provider = (*Provider)(nil)
// New builds a Provider from pc.KVM2. It never dials a host: the credential
// files are only checked for existence, and a missing one is a warning, not
// an error, because the Secret mount is optional and may land later.
func New(ctx context.Context, pc provider.ProviderConfig) (provider.Provider, error) {
cfg := provider.KVMConfig{}
if pc.KVM2 != nil {
cfg = *pc.KVM2
}
cfg = withDefaults(cfg)
p := newProvider(pc.Name, cfg, newLibvirtDialer(pc.Name, cfg))
log := p.logger(ctx)
if _, err := os.Stat(p.cfg.SSHKeyFile); err != nil {
log.Info("WARNING: ssh key file is not readable; every call fails until it is", "path", p.cfg.SSHKeyFile, "reason", err.Error())
}
switch {
case p.cfg.InsecureIgnoreHostKey:
log.Info("WARNING: insecureIgnoreHostKey is set; host keys are not verified")
default:
if _, err := os.Stat(p.cfg.KnownHostsFile); err != nil {
log.Info("WARNING: known_hosts file is not readable; every call fails until it is", "path", p.cfg.KnownHostsFile, "reason", err.Error())
}
}
return p, nil
}
// newProvider is the constructor tests use: d hands out the backends, the
// production one being newLibvirtDialer.
func newProvider(name string, cfg provider.KVMConfig, d dialer) *Provider {
return &Provider{name: name, cfg: withDefaults(cfg), now: time.Now, hosts: newHostTable(d)}
}
// withDefaults returns cfg with every optional field filled in, so the rest
// of the package never re-checks for an empty value. The hosts and images
// are copied, not shared with the caller.
func withDefaults(cfg provider.KVMConfig) provider.KVMConfig {
if cfg.DiskSizeGB == 0 {
cfg.DiskSizeGB = defaultDiskSizeGB
}
hosts := make([]provider.KVMHost, len(cfg.Hosts))
for i, h := range cfg.Hosts {
if h.Socket == "" {
h.Socket = defaultSocket
}
if h.Pool == "" {
h.Pool = defaultPool
}
hosts[i] = h
}
cfg.Hosts = hosts
images := make(map[string]provider.KVMImage, len(cfg.Images))
for k, img := range cfg.Images {
if img.Format == "" {
img.Format = provider.KVMFormatQcow2
}
if img.Firmware == "" {
img.Firmware = provider.KVMFirmwareUEFI
}
images[k] = img
}
cfg.Images = images
return cfg
}
func (p *Provider) logger(ctx context.Context) logr.Logger {
return logf.FromContext(ctx).WithName("kvm2").WithValues("provider", p.name)
}
// host resolves a placement.zone or a providerID's host segment to a
// configured host. An empty zone is allowed only when exactly one host is
// configured.
func (p *Provider) host(zone string) (provider.KVMHost, error) {
if zone == "" {
if len(p.cfg.Hosts) == 1 {
return p.cfg.Hosts[0], nil
}
return provider.KVMHost{}, permanentf("placement.zone is required when kvm2.hosts lists more than one host")
}
for _, h := range p.cfg.Hosts {
if h.Name == zone {
return h, nil
}
}
return provider.KVMHost{}, permanentf("host %q is not in kvm2.hosts", zone)
}
// wrapErr is the single exit for errors: it classifies err and wraps it with
// the configured provider name, op and id.
func (p *Provider) wrapErr(op, id string, err error) error {
return provider.Wrap(classify(op, err), op, p.name, id, err)
}
var errNotImplemented = errors.New("kvm2: not implemented")
// Create is not implemented yet.
func (p *Provider) Create(_ context.Context, _ provider.CreateRequest) (string, error) {
return "", provider.Wrap(provider.ErrPermanent, "create", p.name, "", errNotImplemented)
}
// Get is not implemented yet.
func (p *Provider) Get(_ context.Context, _ string) (*provider.Instance, error) {
return nil, provider.Wrap(provider.ErrPermanent, "get", p.name, "", errNotImplemented)
}
// Delete is not implemented yet.
func (p *Provider) Delete(_ context.Context, _ string) error {
return provider.Wrap(provider.ErrPermanent, "delete", p.name, "", errNotImplemented)
}
// ListByTag is not implemented yet.
func (p *Provider) ListByTag(_ context.Context) ([]provider.Instance, error) {
return nil, provider.Wrap(provider.ErrPermanent, "list", p.name, "", errNotImplemented)
}