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". One source failing is normal and is // skipped: a booting VM has no agent yet, while the host's ARP table // simply reports nothing, since it never sees traffic addressed // elsewhere. A connection already gone, and every source failing, are // errors instead — otherwise a link some other reconcile killed reads as // a running VM that lost its address, and the controller clears its IP // and the health engine drops its accumulated state. func address(ctx context.Context, log logr.Logger, be backend, dom libvirt.Domain) (string, error) { errs := make([]error, 0, len(addressSources)) for _, s := range addressSources { ifaces, err := be.DomainInterfaceAddresses(ctx, dom, uint32(s.src), 0) if err != nil { if isDead(be) || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return "", fmt.Errorf("reading addresses of domain %s from the %s source: %w", dom.Name, s.name, err) } log.V(2).Info("no addresses from source", "source", s.name, "error", err.Error()) errs = append(errs, fmt.Errorf("%s source: %w", s.name, err)) continue } if ip := firstIPv4(ifaces); ip != "" { return ip, nil } } // Transient explicitly: classify would otherwise pick a libvirt code // out of the joined error, and ErrInvalidArg, ErrAccessDenied and // ErrNoSupport are Permanent, which would latch a running proxy as // Failed over an address read. if len(errs) == len(addressSources) { return "", fmt.Errorf("%w: reading addresses of domain %s: every source failed: %w", provider.ErrTransient, dom.Name, errors.Join(errs...)) } return "", nil }