The kvm2 block reuses KVMConfig; validateKVM gains a block-name variant so messages say kvm2. The package skeleton has a non-dialing New, strict providerID parsing, the state table and a 34-row classify test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
98 lines
3.0 KiB
Go
98 lines
3.0 KiB
Go
package kvm2
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/digitalocean/go-libvirt"
|
|
"golang.org/x/crypto/ssh/knownhosts"
|
|
|
|
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
|
|
)
|
|
|
|
// permanentf and transientf classify an error at the source, so classify
|
|
// keeps that class no matter what wraps it later.
|
|
func permanentf(format string, args ...any) error {
|
|
return fmt.Errorf("%w: %s", provider.ErrPermanent, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
func transientf(format string, args ...any) error {
|
|
return fmt.Errorf("%w: %s", provider.ErrTransient, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// sshAuthFailed is the substring x/crypto/ssh puts in every authentication
|
|
// failure ("ssh: handshake failed: ssh: unable to authenticate, attempted
|
|
// methods [...]").
|
|
const sshAuthFailed = "unable to authenticate"
|
|
|
|
// classify maps any error reaching the provider's boundary to one of the
|
|
// four contract sentinels, per spec entry 13. op is the contract operation:
|
|
// a vanished domain or volume inside Create is Transient, because the
|
|
// contract forbids NotFound from Create.
|
|
func classify(op string, err error) error {
|
|
if err == nil {
|
|
return provider.ErrTransient
|
|
}
|
|
for _, class := range []error{provider.ErrNotFound, provider.ErrQuotaExceeded, provider.ErrPermanent, provider.ErrTransient} {
|
|
if errors.Is(err, class) {
|
|
return class
|
|
}
|
|
}
|
|
if code, ok := libvirtCode(err); ok {
|
|
return classifyLibvirt(op, code)
|
|
}
|
|
var keyErr *knownhosts.KeyError
|
|
if errors.As(err, &keyErr) {
|
|
return provider.ErrPermanent
|
|
}
|
|
if strings.Contains(err.Error(), sshAuthFailed) {
|
|
return provider.ErrPermanent
|
|
}
|
|
// Everything else retries: a missing key or known_hosts file (the
|
|
// optional Secret has not landed yet), context cancellation and
|
|
// deadlines, net.OpError, io.EOF, and anything unrecognised.
|
|
return provider.ErrTransient
|
|
}
|
|
|
|
func classifyLibvirt(op string, code libvirt.ErrorNumber) error {
|
|
switch code {
|
|
case libvirt.ErrNoDomain, libvirt.ErrNoStorageVol:
|
|
if op == "create" {
|
|
return provider.ErrTransient
|
|
}
|
|
return provider.ErrNotFound
|
|
case libvirt.ErrNoStoragePool, libvirt.ErrXMLError, libvirt.ErrNoSupport, libvirt.ErrInvalidArg,
|
|
libvirt.ErrAuthFailed, libvirt.ErrAccessDenied:
|
|
return provider.ErrPermanent
|
|
default:
|
|
return provider.ErrTransient
|
|
}
|
|
}
|
|
|
|
// libvirtCode extracts the libvirt error number from err, if it is or wraps
|
|
// a libvirt.Error. That type has a value receiver, so errors.As needs a
|
|
// value target.
|
|
func libvirtCode(err error) (libvirt.ErrorNumber, bool) {
|
|
var le libvirt.Error
|
|
if !errors.As(err, &le) {
|
|
return 0, false
|
|
}
|
|
return libvirt.ErrorNumber(le.Code), true
|
|
}
|
|
|
|
func hasCode(err error, code libvirt.ErrorNumber) bool {
|
|
got, ok := libvirtCode(err)
|
|
return ok && got == code
|
|
}
|
|
|
|
// libvirtErrorFields returns the key/value pairs a log line carries for a
|
|
// libvirt error, or nil when err is not one.
|
|
func libvirtErrorFields(err error) []any {
|
|
code, ok := libvirtCode(err)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return []any{"libvirtCode", int(code), "libvirtError", code.String()}
|
|
}
|