Run 3 on the real host got past Create and failed in Get: libvirt 11.6 selects the <instance> element by our URI in DomainGetMetadata but hands it back with no namespace declaration, and the parser required the URI on the element. The lookup already guarantees the namespace, so match on the local name only. The fake now strips the declaration the way libvirt does, and a new test pins the bare form. Execution log updated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
413 lines
12 KiB
Go
413 lines
12 KiB
Go
package kvm
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
|
|
libvirt "github.com/digitalocean/go-libvirt"
|
|
)
|
|
|
|
// fakeAPI is an in-memory libvirtAPI: pools of volumes and a set of
|
|
// domains, with per-method error injection. It models just enough libvirt
|
|
// semantics for the provider's decisions to be observable — ErrStorageVolExist
|
|
// on duplicate create, ErrNoDomain/ErrNoStorageVol on missing lookups,
|
|
// ErrOperationInvalid on destroying a stopped domain, and so on.
|
|
type fakeAPI struct {
|
|
mu sync.Mutex
|
|
poolPath string
|
|
pools map[string]map[string]*fakeVol
|
|
domains map[string]*fakeDomain
|
|
calls []string
|
|
fail map[string]error
|
|
failOnce map[string]error
|
|
// uploadHook, when set, sees every StorageVolUpload before the fake
|
|
// reads the stream; it can return an error to simulate a broken upload.
|
|
uploadHook func(vol string, r io.Reader) error
|
|
}
|
|
|
|
type fakeVol struct {
|
|
name string
|
|
format string
|
|
capacity int64
|
|
backing string
|
|
data []byte
|
|
}
|
|
|
|
type fakeDomain struct {
|
|
name string
|
|
xml string
|
|
metadata string
|
|
running bool
|
|
// addrs is what DomainInterfaceAddresses returns per source.
|
|
addrs map[uint32][]libvirt.DomainInterface
|
|
}
|
|
|
|
func newFakeAPI(pools ...string) *fakeAPI {
|
|
f := &fakeAPI{
|
|
poolPath: "/var/lib/libvirt/images",
|
|
pools: map[string]map[string]*fakeVol{},
|
|
domains: map[string]*fakeDomain{},
|
|
fail: map[string]error{},
|
|
failOnce: map[string]error{},
|
|
}
|
|
for _, p := range pools {
|
|
f.pools[p] = map[string]*fakeVol{}
|
|
}
|
|
return f
|
|
}
|
|
|
|
// enter records the call and returns any injected error for method.
|
|
func (f *fakeAPI) enter(method string) error {
|
|
f.calls = append(f.calls, method)
|
|
if err, ok := f.failOnce[method]; ok {
|
|
delete(f.failOnce, method)
|
|
return err
|
|
}
|
|
return f.fail[method]
|
|
}
|
|
|
|
func (f *fakeAPI) callCount(method string) int {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
n := 0
|
|
for _, c := range f.calls {
|
|
if c == method {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func (f *fakeAPI) hasCall(method string) bool { return f.callCount(method) > 0 }
|
|
|
|
func (f *fakeAPI) vol(pool, name string) *fakeVol {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.pools[pool][name]
|
|
}
|
|
|
|
func (f *fakeAPI) putVol(pool, name, format string, data []byte) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.pools[pool][name] = &fakeVol{name: name, format: format, capacity: int64(len(data)), data: data}
|
|
}
|
|
|
|
func (f *fakeAPI) volNames(pool string) []string {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
var out []string
|
|
for n := range f.pools[pool] {
|
|
out = append(out, n)
|
|
}
|
|
slices.Sort(out)
|
|
return out
|
|
}
|
|
|
|
func (f *fakeAPI) domain(name string) *fakeDomain {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.domains[name]
|
|
}
|
|
|
|
func (f *fakeAPI) putDomain(d *fakeDomain) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.domains[d.name] = d
|
|
}
|
|
|
|
func lvError(code libvirt.ErrorNumber, format string, args ...any) error {
|
|
return libvirt.Error{Code: uint32(code), Message: fmt.Sprintf(format, args...)}
|
|
}
|
|
|
|
// --- domains ---
|
|
|
|
func (f *fakeAPI) DomainLookupByName(name string) (libvirt.Domain, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if err := f.enter("DomainLookupByName"); err != nil {
|
|
return libvirt.Domain{}, err
|
|
}
|
|
if _, ok := f.domains[name]; !ok {
|
|
return libvirt.Domain{}, lvError(libvirt.ErrNoDomain, "Domain not found: no domain with matching name '%s'", name)
|
|
}
|
|
return libvirt.Domain{Name: name}, nil
|
|
}
|
|
|
|
func (f *fakeAPI) DomainDefineXML(raw string) (libvirt.Domain, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if err := f.enter("DomainDefineXML"); err != nil {
|
|
return libvirt.Domain{}, err
|
|
}
|
|
var d domainXML
|
|
if err := xml.Unmarshal([]byte(raw), &d); err != nil {
|
|
return libvirt.Domain{}, lvError(libvirt.ErrXMLError, "%v", err)
|
|
}
|
|
meta, err := xml.Marshal(d.Metadata.Instance)
|
|
if err != nil {
|
|
return libvirt.Domain{}, lvError(libvirt.ErrXMLError, "%v", err)
|
|
}
|
|
// libvirt hands the element back without its namespace declaration
|
|
// (seen on 11.6); mimic that so Get/ListByTag parse the real shape.
|
|
stored := strings.Replace(string(meta), ` xmlns="`+metadataURI+`"`, "", 1)
|
|
existing := f.domains[d.Name]
|
|
nd := &fakeDomain{name: d.Name, xml: raw, metadata: stored}
|
|
if existing != nil {
|
|
nd.running = existing.running
|
|
nd.addrs = existing.addrs
|
|
}
|
|
f.domains[d.Name] = nd
|
|
return libvirt.Domain{Name: d.Name}, nil
|
|
}
|
|
|
|
func (f *fakeAPI) DomainCreate(d libvirt.Domain) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if err := f.enter("DomainCreate"); err != nil {
|
|
return err
|
|
}
|
|
dom, ok := f.domains[d.Name]
|
|
if !ok {
|
|
return lvError(libvirt.ErrNoDomain, "no domain %s", d.Name)
|
|
}
|
|
if dom.running {
|
|
return lvError(libvirt.ErrOperationInvalid, "domain is already running")
|
|
}
|
|
dom.running = true
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeAPI) DomainDestroy(d libvirt.Domain) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if err := f.enter("DomainDestroy"); err != nil {
|
|
return err
|
|
}
|
|
dom, ok := f.domains[d.Name]
|
|
if !ok {
|
|
return lvError(libvirt.ErrNoDomain, "no domain %s", d.Name)
|
|
}
|
|
if !dom.running {
|
|
return lvError(libvirt.ErrOperationInvalid, "domain is not running")
|
|
}
|
|
dom.running = false
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeAPI) DomainUndefineFlags(d libvirt.Domain, _ libvirt.DomainUndefineFlagsValues) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if err := f.enter("DomainUndefineFlags"); err != nil {
|
|
return err
|
|
}
|
|
if _, ok := f.domains[d.Name]; !ok {
|
|
return lvError(libvirt.ErrNoDomain, "no domain %s", d.Name)
|
|
}
|
|
delete(f.domains, d.Name)
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeAPI) DomainGetState(d libvirt.Domain, _ uint32) (int32, int32, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if err := f.enter("DomainGetState"); err != nil {
|
|
return 0, 0, err
|
|
}
|
|
dom, ok := f.domains[d.Name]
|
|
if !ok {
|
|
return 0, 0, lvError(libvirt.ErrNoDomain, "no domain %s", d.Name)
|
|
}
|
|
if dom.running {
|
|
return int32(libvirt.DomainRunning), 0, nil
|
|
}
|
|
return int32(libvirt.DomainShutoff), 0, nil
|
|
}
|
|
|
|
func (f *fakeAPI) DomainGetMetadata(d libvirt.Domain, typ int32, uri libvirt.OptString, _ libvirt.DomainModificationImpact) (string, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if err := f.enter("DomainGetMetadata"); err != nil {
|
|
return "", err
|
|
}
|
|
dom, ok := f.domains[d.Name]
|
|
if !ok {
|
|
return "", lvError(libvirt.ErrNoDomain, "no domain %s", d.Name)
|
|
}
|
|
if typ != int32(libvirt.DomainMetadataElement) || len(uri) != 1 || uri[0] != metadataURI || dom.metadata == "" {
|
|
return "", lvError(libvirt.ErrNoDomainMetadata, "Requested metadata element is not present")
|
|
}
|
|
return dom.metadata, nil
|
|
}
|
|
|
|
func (f *fakeAPI) DomainInterfaceAddresses(d libvirt.Domain, source uint32, _ uint32) ([]libvirt.DomainInterface, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if err := f.enter("DomainInterfaceAddresses"); err != nil {
|
|
return nil, err
|
|
}
|
|
dom, ok := f.domains[d.Name]
|
|
if !ok {
|
|
return nil, lvError(libvirt.ErrNoDomain, "no domain %s", d.Name)
|
|
}
|
|
if !dom.running {
|
|
return nil, lvError(libvirt.ErrOperationInvalid, "domain is not running")
|
|
}
|
|
if source == uint32(libvirt.DomainInterfaceAddressesSrcAgent) && dom.addrs[source] == nil {
|
|
return nil, lvError(libvirt.ErrAgentUnresponsive, "Guest agent is not responding")
|
|
}
|
|
return dom.addrs[source], nil
|
|
}
|
|
|
|
func (f *fakeAPI) ConnectListAllDomains(_ int32, _ libvirt.ConnectListAllDomainsFlags) ([]libvirt.Domain, uint32, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if err := f.enter("ConnectListAllDomains"); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
names := make([]string, 0, len(f.domains))
|
|
for n := range f.domains {
|
|
names = append(names, n)
|
|
}
|
|
slices.Sort(names)
|
|
out := make([]libvirt.Domain, 0, len(names))
|
|
for _, n := range names {
|
|
out = append(out, libvirt.Domain{Name: n})
|
|
}
|
|
return out, uint32(len(out)), nil
|
|
}
|
|
|
|
// --- storage ---
|
|
|
|
func (f *fakeAPI) StoragePoolLookupByName(name string) (libvirt.StoragePool, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if err := f.enter("StoragePoolLookupByName"); err != nil {
|
|
return libvirt.StoragePool{}, err
|
|
}
|
|
if _, ok := f.pools[name]; !ok {
|
|
return libvirt.StoragePool{}, lvError(libvirt.ErrNoStoragePool, "Storage pool not found: no storage pool with matching name '%s'", name)
|
|
}
|
|
return libvirt.StoragePool{Name: name}, nil
|
|
}
|
|
|
|
func (f *fakeAPI) StoragePoolRefresh(pool libvirt.StoragePool, _ uint32) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if err := f.enter("StoragePoolRefresh"); err != nil {
|
|
return err
|
|
}
|
|
if _, ok := f.pools[pool.Name]; !ok {
|
|
return lvError(libvirt.ErrNoStoragePool, "no pool %s", pool.Name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeAPI) StorageVolLookupByName(pool libvirt.StoragePool, name string) (libvirt.StorageVol, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if err := f.enter("StorageVolLookupByName"); err != nil {
|
|
return libvirt.StorageVol{}, err
|
|
}
|
|
if _, ok := f.pools[pool.Name][name]; !ok {
|
|
return libvirt.StorageVol{}, lvError(libvirt.ErrNoStorageVol, "Storage volume not found: no storage vol with matching name '%s'", name)
|
|
}
|
|
return libvirt.StorageVol{Pool: pool.Name, Name: name, Key: f.poolPath + "/" + name}, nil
|
|
}
|
|
|
|
func (f *fakeAPI) StorageVolGetPath(vol libvirt.StorageVol) (string, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if err := f.enter("StorageVolGetPath"); err != nil {
|
|
return "", err
|
|
}
|
|
if _, ok := f.pools[vol.Pool][vol.Name]; !ok {
|
|
return "", lvError(libvirt.ErrNoStorageVol, "no vol %s", vol.Name)
|
|
}
|
|
return f.poolPath + "/" + vol.Name, nil
|
|
}
|
|
|
|
func (f *fakeAPI) StorageVolCreateXML(pool libvirt.StoragePool, raw string, _ libvirt.StorageVolCreateFlags) (libvirt.StorageVol, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if err := f.enter("StorageVolCreateXML"); err != nil {
|
|
return libvirt.StorageVol{}, err
|
|
}
|
|
vols, ok := f.pools[pool.Name]
|
|
if !ok {
|
|
return libvirt.StorageVol{}, lvError(libvirt.ErrNoStoragePool, "no pool %s", pool.Name)
|
|
}
|
|
var v volumeXML
|
|
if err := xml.Unmarshal([]byte(raw), &v); err != nil {
|
|
return libvirt.StorageVol{}, lvError(libvirt.ErrXMLError, "%v", err)
|
|
}
|
|
if _, exists := vols[v.Name]; exists {
|
|
return libvirt.StorageVol{}, lvError(libvirt.ErrStorageVolExist, "storage volume '%s' exists already", v.Name)
|
|
}
|
|
fv := &fakeVol{name: v.Name, format: v.Target.Format.Type, capacity: v.Capacity.Value}
|
|
if v.BackingStore != nil {
|
|
fv.backing = v.BackingStore.Path
|
|
}
|
|
vols[v.Name] = fv
|
|
return libvirt.StorageVol{Pool: pool.Name, Name: v.Name, Key: f.poolPath + "/" + v.Name}, nil
|
|
}
|
|
|
|
func (f *fakeAPI) StorageVolUpload(vol libvirt.StorageVol, r io.Reader, _, _ uint64, _ libvirt.StorageVolUploadFlags) error {
|
|
f.mu.Lock()
|
|
if err := f.enter("StorageVolUpload"); err != nil {
|
|
f.mu.Unlock()
|
|
return err
|
|
}
|
|
fv, ok := f.pools[vol.Pool][vol.Name]
|
|
hook := f.uploadHook
|
|
f.mu.Unlock()
|
|
if !ok {
|
|
return lvError(libvirt.ErrNoStorageVol, "no vol %s", vol.Name)
|
|
}
|
|
if hook != nil {
|
|
if err := hook(vol.Name, r); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// Read outside the lock: the stream may be a slow HTTP body.
|
|
data, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return fmt.Errorf("reading upload stream: %w", err)
|
|
}
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
fv.data = data
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeAPI) StorageVolDownload(vol libvirt.StorageVol, w io.Writer, _, _ uint64, _ libvirt.StorageVolDownloadFlags) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if err := f.enter("StorageVolDownload"); err != nil {
|
|
return err
|
|
}
|
|
fv, ok := f.pools[vol.Pool][vol.Name]
|
|
if !ok {
|
|
return lvError(libvirt.ErrNoStorageVol, "no vol %s", vol.Name)
|
|
}
|
|
_, err := io.Copy(w, bytes.NewReader(fv.data))
|
|
return err
|
|
}
|
|
|
|
func (f *fakeAPI) StorageVolDelete(vol libvirt.StorageVol, _ libvirt.StorageVolDeleteFlags) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if err := f.enter("StorageVolDelete"); err != nil {
|
|
return err
|
|
}
|
|
if _, ok := f.pools[vol.Pool][vol.Name]; !ok {
|
|
return lvError(libvirt.ErrNoStorageVol, "no vol %s", vol.Name)
|
|
}
|
|
delete(f.pools[vol.Pool], vol.Name)
|
|
return nil
|
|
}
|