Shorter start wait with immediate verdicts for dead or paused domains, ListByTag without state or address RPCs, Timeout for an expired check round, no adoption of unmarked domains, reference-counted keyed locks, dead options removed, HEAD fallback on any non-2xx. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
120 lines
4.0 KiB
Go
120 lines
4.0 KiB
Go
package kvm2
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/digitalocean/go-libvirt"
|
|
"github.com/go-logr/logr"
|
|
|
|
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
|
|
)
|
|
|
|
// Get is spec entries 7 to 9: look the domain up by name, read the mark
|
|
// back, then state and address. Only ErrNoDomain becomes NotFound; every
|
|
// other failure is classified, never guessed.
|
|
func (p *Provider) Get(ctx context.Context, providerID string) (*provider.Instance, error) {
|
|
hostName, name, err := parseProviderID(providerID)
|
|
if err != nil {
|
|
return nil, provider.Wrap(provider.ErrPermanent, "get", p.name, providerID, err)
|
|
}
|
|
host, err := p.host(hostName)
|
|
if err != nil {
|
|
return nil, p.wrapErr("get", providerID, err)
|
|
}
|
|
log := p.logger(ctx).WithValues("host", host.Name, "name", name)
|
|
be, err := p.hosts.get(ctx, host)
|
|
if err != nil {
|
|
logAPIError(log, "get", err)
|
|
return nil, p.wrapErr("get", providerID, err)
|
|
}
|
|
inst, err := get(ctx, log, be, host.Name, name)
|
|
if err != nil {
|
|
if hasCode(err, libvirt.ErrNoDomain) {
|
|
log.V(1).Info("kvm2 domain not found")
|
|
} else {
|
|
logAPIError(log, "get", err)
|
|
}
|
|
return nil, p.wrapErr("get", providerID, err)
|
|
}
|
|
log.V(1).Info("kvm2 domain fetched", "state", inst.State, "ip", inst.IP, "uid", inst.UID)
|
|
return inst, nil
|
|
}
|
|
|
|
// get builds the Instance from the domain's metadata, its state and, for an
|
|
// active domain, its discovered address.
|
|
func get(ctx context.Context, log logr.Logger, be backend, hostName, name string) (*provider.Instance, error) {
|
|
dom, err := be.DomainLookupByName(ctx, name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("looking up domain %s: %w", name, err)
|
|
}
|
|
meta, err := readMetadata(ctx, be, dom)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
state, _, err := be.DomainGetState(ctx, dom, 0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading state of domain %s: %w", dom.Name, err)
|
|
}
|
|
s := libvirt.DomainState(state)
|
|
var ip string
|
|
if isActive(s) {
|
|
if ip, err = address(ctx, log, be, dom); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &provider.Instance{
|
|
ID: formatProviderID(hostName, dom.Name),
|
|
IP: ip,
|
|
State: mapState(s, ip),
|
|
UID: meta.UID,
|
|
CreatedAt: meta.createdAt(),
|
|
}, nil
|
|
}
|
|
|
|
// readMetadata returns the kvm2 mark of a domain, or the zero value (not
|
|
// managed, empty UID) when the domain carries none. A mark that does not
|
|
// parse is Permanent, from parseInstanceMetadata.
|
|
func readMetadata(ctx context.Context, be backend, dom libvirt.Domain) (instanceMetadata, error) {
|
|
raw, err := be.DomainGetMetadata(ctx, dom, int32(libvirt.DomainMetadataElement), libvirt.OptString{metadataURI}, libvirt.DomainAffectCurrent)
|
|
if hasCode(err, libvirt.ErrNoDomainMetadata) {
|
|
return instanceMetadata{}, nil
|
|
}
|
|
if err != nil {
|
|
return instanceMetadata{}, fmt.Errorf("reading metadata of domain %s: %w", dom.Name, err)
|
|
}
|
|
return parseInstanceMetadata(raw)
|
|
}
|
|
|
|
// addressSources is the discovery order of spec entry 7: the guest agent
|
|
// answers where the host's ARP table never sees guest traffic.
|
|
var addressSources = []struct {
|
|
name string
|
|
src libvirt.DomainInterfaceAddressesSource
|
|
}{
|
|
{"agent", libvirt.DomainInterfaceAddressesSrcAgent},
|
|
{"arp", libvirt.DomainInterfaceAddressesSrcArp},
|
|
}
|
|
|
|
// address returns the first usable IPv4 from the first source that yields
|
|
// one, or "" for "no address yet". A source that fails is logged and
|
|
// skipped; only ctx ending is an error, so a cancelled Get does not read
|
|
// as Provisioning.
|
|
func address(ctx context.Context, log logr.Logger, be backend, dom libvirt.Domain) (string, error) {
|
|
for _, s := range addressSources {
|
|
ifaces, err := be.DomainInterfaceAddresses(ctx, dom, uint32(s.src), 0)
|
|
if err != nil {
|
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
|
return "", fmt.Errorf("reading addresses of domain %s: %w", dom.Name, err)
|
|
}
|
|
log.V(2).Info("no addresses from source", "source", s.name, "error", err.Error())
|
|
continue
|
|
}
|
|
if ip := firstIPv4(ifaces); ip != "" {
|
|
return ip, nil
|
|
}
|
|
}
|
|
return "", nil
|
|
}
|