KVM/libvirt provider: proxies as VMs on your own hypervisors #5
@@ -81,7 +81,9 @@ The metadata element is written with a default namespace
|
|||||||
marshal. libvirt may hand it back prefixed; `parseInstanceMetadata` is
|
marshal. libvirt may hand it back prefixed; `parseInstanceMetadata` is
|
||||||
tested against both spellings since `encoding/xml` matches on the namespace
|
tested against both spellings since `encoding/xml` matches on the namespace
|
||||||
URI. Whether libvirt accepts the default-namespace form on `DomainDefineXML`
|
URI. Whether libvirt accepts the default-namespace form on `DomainDefineXML`
|
||||||
is confirmed in Step 6 against a real host, not here.
|
is confirmed in Step 6 against a real host, not here. (Step 7: it does,
|
||||||
|
but `DomainGetMetadata` returns the element with no namespace at all; the
|
||||||
|
parser now matches on local name only.)
|
||||||
|
|
||||||
Checked before relying on `github.com/kdomanski/iso9660` for the seed: its
|
Checked before relying on `github.com/kdomanski/iso9660` for the seed: its
|
||||||
filename mangling lowercases and keeps `-`, so `user-data`/`meta-data`/
|
filename mangling lowercases and keeps `-`, so `user-data`/`meta-data`/
|
||||||
@@ -445,3 +447,18 @@ paths the fake reports.
|
|||||||
|
|
||||||
Worth noting: the first two runs left the base image and its marker in
|
Worth noting: the first two runs left the base image and its marker in
|
||||||
the pool as designed, so later runs skip the 435 MB download.
|
the pool as designed, so later runs skip the 435 MB download.
|
||||||
|
|
||||||
|
### Run 3: `parsing domain metadata: expected element <instance> in name space … but have no name space`
|
||||||
|
|
||||||
|
With file-backed disks, Create returned in 1 s (image cached) and the
|
||||||
|
domain started. Get then failed to parse the metadata: libvirt 11.6 finds
|
||||||
|
the element by our URI but returns it as a bare `<instance>` with no
|
||||||
|
namespace declaration at all. Step 2 had tested the default-namespace and
|
||||||
|
prefixed spellings and left "does libvirt accept the default-namespace
|
||||||
|
form" for the real host; it does, it just does not give it back that way.
|
||||||
|
|
||||||
|
Fix: `parseInstanceMetadata` matches on the local name only, since the
|
||||||
|
lookup already selected the element by URI. The fake now strips the
|
||||||
|
declaration when storing metadata, so the provider's Get and ListByTag
|
||||||
|
tests parse the shape libvirt really returns, and a new
|
||||||
|
`TestParseInstanceMetadata_libvirtBareForm` pins it.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
libvirt "github.com/digitalocean/go-libvirt"
|
libvirt "github.com/digitalocean/go-libvirt"
|
||||||
@@ -151,8 +152,11 @@ func (f *fakeAPI) DomainDefineXML(raw string) (libvirt.Domain, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return libvirt.Domain{}, lvError(libvirt.ErrXMLError, "%v", err)
|
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]
|
existing := f.domains[d.Name]
|
||||||
nd := &fakeDomain{name: d.Name, xml: raw, metadata: string(meta)}
|
nd := &fakeDomain{name: d.Name, xml: raw, metadata: stored}
|
||||||
if existing != nil {
|
if existing != nil {
|
||||||
nd.running = existing.running
|
nd.running = existing.running
|
||||||
nd.addrs = existing.addrs
|
nd.addrs = existing.addrs
|
||||||
|
|||||||
@@ -31,15 +31,21 @@ func newInstanceMetadata(uid string, createdAt time.Time) instanceMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parseInstanceMetadata decodes the element as DomainGetMetadata returns it.
|
// parseInstanceMetadata decodes the element as DomainGetMetadata returns it.
|
||||||
// libvirt may re-serialise it with a namespace prefix rather than the default
|
// libvirt selects the element by namespace URI but serialises it without the
|
||||||
// namespace it was defined with; encoding/xml matches on namespace URI, so
|
// declaration (observed on libvirt 11.6: a bare <instance>), and other
|
||||||
// both spellings decode.
|
// versions may keep a default or prefixed form. The URI is already checked
|
||||||
|
// by the lookup, so only the local name is matched here.
|
||||||
func parseInstanceMetadata(s string) (instanceMetadata, error) {
|
func parseInstanceMetadata(s string) (instanceMetadata, error) {
|
||||||
var m instanceMetadata
|
var raw struct {
|
||||||
if err := xml.Unmarshal([]byte(s), &m); err != nil {
|
XMLName xml.Name `xml:"instance"`
|
||||||
|
Managed string `xml:"managed"`
|
||||||
|
UID string `xml:"uid"`
|
||||||
|
CreatedAt string `xml:"createdAt"`
|
||||||
|
}
|
||||||
|
if err := xml.Unmarshal([]byte(s), &raw); err != nil {
|
||||||
return instanceMetadata{}, fmt.Errorf("parsing domain metadata: %w", err)
|
return instanceMetadata{}, fmt.Errorf("parsing domain metadata: %w", err)
|
||||||
}
|
}
|
||||||
return m, nil
|
return instanceMetadata{Managed: raw.Managed, UID: raw.UID, CreatedAt: raw.CreatedAt}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m instanceMetadata) managed() bool {
|
func (m instanceMetadata) managed() bool {
|
||||||
|
|||||||
@@ -45,6 +45,21 @@ func TestParseInstanceMetadata_libvirtPrefixedForm(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseInstanceMetadata_libvirtBareForm(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
// What libvirt 11.6 actually returns from DomainGetMetadata: the element
|
||||||
|
// selected by our URI, serialised without any namespace declaration.
|
||||||
|
in := `<instance><managed>true</managed><uid>uid-3</uid>` +
|
||||||
|
`<createdAt>2026-09-10T07:30:00Z</createdAt></instance>`
|
||||||
|
m, err := parseInstanceMetadata(in)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseInstanceMetadata() error = %v", err)
|
||||||
|
}
|
||||||
|
if !m.managed() || m.UID != "uid-3" || m.createdAt().Minute() != 30 {
|
||||||
|
t.Errorf("parsed = %+v", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseInstanceMetadata_notManaged(t *testing.T) {
|
func TestParseInstanceMetadata_notManaged(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user