diff --git a/charts/kvm-node-agent/Chart.yaml b/charts/kvm-node-agent/Chart.yaml
index 838c05a..399036e 100644
--- a/charts/kvm-node-agent/Chart.yaml
+++ b/charts/kvm-node-agent/Chart.yaml
@@ -2,5 +2,5 @@ apiVersion: v2
name: kvm-node-agent
description: A Helm chart for Kubernetes
appVersion: 0.1.0
-version: 0.1.7
+version: 0.1.6
type: application
diff --git a/charts/kvm-node-agent/crds/hypervisor-crd.yaml b/charts/kvm-node-agent/crds/hypervisor-crd.yaml
index 311ba15..dad29e5 100644
--- a/charts/kvm-node-agent/crds/hypervisor-crd.yaml
+++ b/charts/kvm-node-agent/crds/hypervisor-crd.yaml
@@ -110,6 +110,15 @@ spec:
items:
type: string
type: array
+ allowedProjects:
+ default: []
+ description: |-
+ AllowedProjects defines which openstack projects are allowed to schedule
+ instances on this hypervisor. The values of this list should be project
+ uuids. If left empty, all projects are allowed.
+ items:
+ type: string
+ type: array
createCertManagerCertificate:
default: false
description: |-
@@ -167,6 +176,7 @@ spec:
type: string
required:
- aggregates
+ - allowedProjects
- createCertManagerCertificate
- customTraits
- evacuateOnReboot
@@ -184,8 +194,17 @@ spec:
items:
type: string
type: array
+ allocation:
+ additionalProperties:
+ anyOf:
+ - type: integer
+ - type: string
+ pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
+ x-kubernetes-int-or-string: true
+ description: Auto-discovered resource allocation of all hosted VMs.
+ type: object
capabilities:
- description: The capabilities of the hypervisors as reported by libvirt.
+ description: Auto-discovered capabilities as reported by libvirt.
properties:
cpuArch:
default: unknown
@@ -208,6 +227,47 @@ spec:
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
type: object
+ capacity:
+ additionalProperties:
+ anyOf:
+ - type: integer
+ - type: string
+ pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
+ x-kubernetes-int-or-string: true
+ description: Auto-discovered capacity of the hypervisor.
+ type: object
+ cells:
+ description: Auto-discovered cells on this hypervisor.
+ items:
+ description: Cell represents a NUMA cell on the hypervisor.
+ properties:
+ allocation:
+ additionalProperties:
+ anyOf:
+ - type: integer
+ - type: string
+ pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
+ x-kubernetes-int-or-string: true
+ description: Auto-discovered resource allocation of all hosted
+ VMs in this cell.
+ type: object
+ capacity:
+ additionalProperties:
+ anyOf:
+ - type: integer
+ - type: string
+ pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
+ x-kubernetes-int-or-string: true
+ description: Auto-discovered capacity of this cell.
+ type: object
+ cellID:
+ description: Cell ID.
+ format: int64
+ type: integer
+ required:
+ - cellID
+ type: object
+ type: array
conditions:
description: Represents the Hypervisor node conditions.
items:
@@ -265,6 +325,72 @@ spec:
- type
type: object
type: array
+ domainCapabilities:
+ description: |-
+ Auto-discovered domain capabilities relevant to check if a VM
+ can be scheduled on the hypervisor.
+ properties:
+ arch:
+ default: unknown
+ description: The available domain cpu architecture.
+ type: string
+ hypervisorType:
+ default: unknown
+ description: The supported type of virtualization for domains,
+ such as "ch".
+ type: string
+ supportedCpuModes:
+ default: []
+ description: |-
+ Supported cpu modes for domains.
+
+ The format of this list is cpu mode, and if specified, a specific
+ submode. For example, the take the following xml domain cpu definition:
+
+
+
+
+
+ The corresponding entries in this list would be "host-passthrough" and
+ "host-passthrough/migratable".
+ items:
+ type: string
+ type: array
+ supportedDevices:
+ default: []
+ description: |-
+ Supported devices for domains.
+
+ The format of this list is the device type, and if specified, a specific
+ model. For example, the take the following xml domain device definition:
+
+
+
+ The corresponding entries in this list would be "video" and "video/nvidia".
+ items:
+ type: string
+ type: array
+ supportedFeatures:
+ default: []
+ description: |-
+ Supported features for domains, such as "sev" or "sgx".
+
+ This is a flat list of supported features, meaning the following xml:
+
+
+
+
+
+
+ Would correspond to the entries "sev" and "sgx" in this list.
+ items:
+ type: string
+ type: array
+ type: object
evicted:
description: Evicted indicates whether the hypervisor is evicted.
(no instances left with active maintenance mode)
diff --git a/config/crd/bases/kvm.cloud.sap_hypervisors.yaml b/config/crd/bases/kvm.cloud.sap_hypervisors.yaml
index ac050ff..dad29e5 100644
--- a/config/crd/bases/kvm.cloud.sap_hypervisors.yaml
+++ b/config/crd/bases/kvm.cloud.sap_hypervisors.yaml
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
- controller-gen.kubebuilder.io/version: v0.16.1
+ controller-gen.kubebuilder.io/version: v0.19.0
name: hypervisors.kvm.cloud.sap
spec:
group: kvm.cloud.sap
@@ -35,6 +35,9 @@ spec:
- jsonPath: .status.conditions[?(@.type=="Ready")].reason
name: State
type: string
+ - jsonPath: .status.conditions[?(@.type=="Tainted")].message
+ name: Taint
+ type: string
- jsonPath: .spec.lifecycleEnabled
name: Lifecycle
type: boolean
@@ -107,6 +110,15 @@ spec:
items:
type: string
type: array
+ allowedProjects:
+ default: []
+ description: |-
+ AllowedProjects defines which openstack projects are allowed to schedule
+ instances on this hypervisor. The values of this list should be project
+ uuids. If left empty, all projects are allowed.
+ items:
+ type: string
+ type: array
createCertManagerCertificate:
default: false
description: |-
@@ -139,6 +151,16 @@ spec:
description: LifecycleEnabled enables the lifecycle management of
the hypervisor via hypervisor-operator.
type: boolean
+ maintenance:
+ description: Maintenance indicates whether the hypervisor is in maintenance
+ mode.
+ enum:
+ - ""
+ - manual
+ - auto
+ - ha
+ - termination
+ type: string
reboot:
default: false
description: Reboot request an reboot after successful installation
@@ -154,6 +176,7 @@ spec:
type: string
required:
- aggregates
+ - allowedProjects
- createCertManagerCertificate
- customTraits
- evacuateOnReboot
@@ -171,8 +194,17 @@ spec:
items:
type: string
type: array
+ allocation:
+ additionalProperties:
+ anyOf:
+ - type: integer
+ - type: string
+ pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
+ x-kubernetes-int-or-string: true
+ description: Auto-discovered resource allocation of all hosted VMs.
+ type: object
capabilities:
- description: The capabilities of the hypervisors as reported by libvirt.
+ description: Auto-discovered capabilities as reported by libvirt.
properties:
cpuArch:
default: unknown
@@ -195,6 +227,47 @@ spec:
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
type: object
+ capacity:
+ additionalProperties:
+ anyOf:
+ - type: integer
+ - type: string
+ pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
+ x-kubernetes-int-or-string: true
+ description: Auto-discovered capacity of the hypervisor.
+ type: object
+ cells:
+ description: Auto-discovered cells on this hypervisor.
+ items:
+ description: Cell represents a NUMA cell on the hypervisor.
+ properties:
+ allocation:
+ additionalProperties:
+ anyOf:
+ - type: integer
+ - type: string
+ pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
+ x-kubernetes-int-or-string: true
+ description: Auto-discovered resource allocation of all hosted
+ VMs in this cell.
+ type: object
+ capacity:
+ additionalProperties:
+ anyOf:
+ - type: integer
+ - type: string
+ pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
+ x-kubernetes-int-or-string: true
+ description: Auto-discovered capacity of this cell.
+ type: object
+ cellID:
+ description: Cell ID.
+ format: int64
+ type: integer
+ required:
+ - cellID
+ type: object
+ type: array
conditions:
description: Represents the Hypervisor node conditions.
items:
@@ -252,6 +325,76 @@ spec:
- type
type: object
type: array
+ domainCapabilities:
+ description: |-
+ Auto-discovered domain capabilities relevant to check if a VM
+ can be scheduled on the hypervisor.
+ properties:
+ arch:
+ default: unknown
+ description: The available domain cpu architecture.
+ type: string
+ hypervisorType:
+ default: unknown
+ description: The supported type of virtualization for domains,
+ such as "ch".
+ type: string
+ supportedCpuModes:
+ default: []
+ description: |-
+ Supported cpu modes for domains.
+
+ The format of this list is cpu mode, and if specified, a specific
+ submode. For example, the take the following xml domain cpu definition:
+
+
+
+
+
+ The corresponding entries in this list would be "host-passthrough" and
+ "host-passthrough/migratable".
+ items:
+ type: string
+ type: array
+ supportedDevices:
+ default: []
+ description: |-
+ Supported devices for domains.
+
+ The format of this list is the device type, and if specified, a specific
+ model. For example, the take the following xml domain device definition:
+
+
+
+ The corresponding entries in this list would be "video" and "video/nvidia".
+ items:
+ type: string
+ type: array
+ supportedFeatures:
+ default: []
+ description: |-
+ Supported features for domains, such as "sev" or "sgx".
+
+ This is a flat list of supported features, meaning the following xml:
+
+
+
+
+
+
+ Would correspond to the entries "sev" and "sgx" in this list.
+ items:
+ type: string
+ type: array
+ type: object
+ evicted:
+ description: Evicted indicates whether the hypervisor is evicted.
+ (no instances left with active maintenance mode)
+ type: boolean
hypervisorId:
description: HypervisorID is the unique identifier of the hypervisor
in OpenStack.
@@ -299,6 +442,14 @@ spec:
firmwareVersion:
description: FirmwareVersion
type: string
+ gardenLinuxCommitID:
+ description: Represents the Garden Linux build commit id
+ type: string
+ gardenLinuxFeatures:
+ description: Represents the Garden Linux Feature Set
+ items:
+ type: string
+ type: array
hardwareModel:
description: HardwareModel
type: string
@@ -320,20 +471,13 @@ spec:
prettyVersion:
description: PrettyVersion
type: string
+ variantID:
+ description: Identifying a specific variant or edition of the
+ operating system
+ type: string
version:
description: Represents the Operating System version.
type: string
- gardenLinuxCommitID:
- description: GardenLinuxCommitID
- type: string
- gardenLinuxFeatures:
- description: GardenLinuxFeatures
- items:
- type: string
- type: array
- variantID:
- description: VariantID
- type: string
type: object
serviceId:
description: ServiceID is the unique identifier of the compute service
diff --git a/go.mod b/go.mod
index 913b064..a84e21b 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ go 1.25.0
require (
github.com/cert-manager/cert-manager v1.19.2
- github.com/cobaltcore-dev/openstack-hypervisor-operator v0.0.0-20251219152336-768f63171244
+ github.com/cobaltcore-dev/openstack-hypervisor-operator v0.0.0-20260107080351-e998a1af4394
github.com/coreos/go-systemd/v22 v22.6.0
github.com/digitalocean/go-libvirt v0.0.0-20260105165635-a0e369cfdc9f
github.com/godbus/dbus/v5 v5.2.2
diff --git a/go.sum b/go.sum
index 5ff9f31..b9a8634 100644
--- a/go.sum
+++ b/go.sum
@@ -14,8 +14,8 @@ github.com/cert-manager/cert-manager v1.19.2 h1:jSprN1h5pgNDSl7HClAmIzXuTxic/5FX
github.com/cert-manager/cert-manager v1.19.2/go.mod h1:e9NzLtOKxTw7y99qLyWGmPo6mrC1Nh0EKKcMkRfK+GE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/cobaltcore-dev/openstack-hypervisor-operator v0.0.0-20251219152336-768f63171244 h1:HedVhcR2smWlJqthYHYT5kL3Hhqjvg3lETz3pWiDprc=
-github.com/cobaltcore-dev/openstack-hypervisor-operator v0.0.0-20251219152336-768f63171244/go.mod h1:i/YQm59sAvilkgTFpKc+elMIf/KzkdimnXMd13P3V9s=
+github.com/cobaltcore-dev/openstack-hypervisor-operator v0.0.0-20260107080351-e998a1af4394 h1:GWyHLNTwXp4m+D7OGuQwtIWGRPUBCEjVSowK4CgrdQU=
+github.com/cobaltcore-dev/openstack-hypervisor-operator v0.0.0-20260107080351-e998a1af4394/go.mod h1:i/YQm59sAvilkgTFpKc+elMIf/KzkdimnXMd13P3V9s=
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
diff --git a/internal/controller/hypervisor_controller.go b/internal/controller/hypervisor_controller.go
index 9e0bf5c..5c21fdb 100644
--- a/internal/controller/hypervisor_controller.go
+++ b/internal/controller/hypervisor_controller.go
@@ -51,9 +51,8 @@ type HypervisorReconciler struct {
}
const (
- OSUpdateType = "OperatingSystemUpdate"
- LibVirtType = "LibVirtConnection"
- CapabilitiesClientType = "CapabilitiesClientConnection"
+ OSUpdateType = "OperatingSystemUpdate"
+ LibVirtType = "LibVirtConnection"
)
// +kubebuilder:rbac:groups=kvm.cloud.sap,resources=hypervisors,verbs=get;list;watch;update;patch;delete
@@ -182,35 +181,24 @@ func (r *HypervisorReconciler) Reconcile(ctx context.Context, req ctrl.Request)
Reason: "ConnectFailed",
})
} else {
- hypervisor.Status.LibVirtVersion = r.Libvirt.GetVersion()
+ // We're connected.
meta.SetStatusCondition(&hypervisor.Status.Conditions, metav1.Condition{
Type: LibVirtType,
Status: metav1.ConditionTrue,
Reason: "Connected",
})
- // Update hypervisor instances
- hypervisor.Status.NumInstances = r.Libvirt.GetNumInstances()
- if hypervisor.Status.Instances, err = r.Libvirt.GetInstances(); err != nil {
- log.Error(err, "failed to get instances")
- }
-
- // Update capabilities status.
- if capabilities, err := r.Libvirt.GetCapabilities(); err == nil {
- hypervisor.Status.Capabilities = capabilities
- meta.SetStatusCondition(&hypervisor.Status.Conditions, metav1.Condition{
- Type: CapabilitiesClientType,
- Status: metav1.ConditionTrue,
- Reason: "CapabilitiesClientGetSucceeded",
- })
- } else {
- log.Error(err, "failed to get capabilities")
+ var err error
+ hypervisor, err = r.Libvirt.Process(hypervisor)
+ if err != nil {
+ log.Error(err, "unable to process hypervisor via libvirt")
meta.SetStatusCondition(&hypervisor.Status.Conditions, metav1.Condition{
- Type: CapabilitiesClientType,
+ Type: LibVirtType,
Status: metav1.ConditionFalse,
- Message: err.Error(),
- Reason: "CapabilitiesClientGetFailed",
+ Message: fmt.Sprintf("unable to process hypervisor via libvirt: %v", err),
+ Reason: "ProcessFailed",
})
+ return ctrl.Result{}, err
}
}
diff --git a/internal/controller/hypervisor_controller_test.go b/internal/controller/hypervisor_controller_test.go
index 61f5ba8..69ea729 100644
--- a/internal/controller/hypervisor_controller_test.go
+++ b/internal/controller/hypervisor_controller_test.go
@@ -22,7 +22,6 @@ import (
kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
"github.com/coreos/go-systemd/v22/dbus"
- golibvirt "github.com/digitalocean/go-libvirt"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/api/errors"
@@ -87,33 +86,27 @@ var _ = Describe("Hypervisor Controller", func() {
ConnectFunc: func() error {
return nil
},
- GetDomainsActiveFunc: func() ([]golibvirt.Domain, error) {
- return []golibvirt.Domain{}, nil
- },
- GetInstancesFunc: func() ([]kvmv1.Instance, error) {
- return []kvmv1.Instance{
+ ProcessFunc: func(hv kvmv1.Hypervisor) (kvmv1.Hypervisor, error) {
+ hv.Status.Instances = []kvmv1.Instance{
{
ID: "25e2ea06-f6be-4bac-856d-8c2d0bdbcdee",
Name: "test-instance",
Active: false,
},
- }, nil
- },
- IsConnectedFunc: func() bool {
- return true
- },
- GetVersionFunc: func() string {
- return "10.9.0"
- },
- GetNumInstancesFunc: func() int {
- return 1
- },
- GetCapabilitiesFunc: func() (kvmv1.CapabilitiesStatus, error) {
- return kvmv1.CapabilitiesStatus{
+ }
+ hv.Status.LibVirtVersion = "10.9.0"
+ hv.Status.NumInstances = 1
+ hv.Status.Capabilities = kvmv1.Capabilities{
HostCpuArch: "x86_64",
HostCpus: *resource.NewQuantity(4, resource.DecimalSI),
HostMemory: *resource.NewQuantity(8192, resource.DecimalSI),
- }, nil
+ }
+ hv.Status.DomainCapabilities = kvmv1.DomainCapabilities{
+ Arch: "x86_64",
+ HypervisorType: "kvm",
+ SupportedCpuModes: []string{"mode/example", "mode/example/1"},
+ }
+ return hv, nil
},
},
Systemd: &systemd.InterfaceMock{
@@ -155,7 +148,7 @@ var _ = Describe("Hypervisor Controller", func() {
Expect(hypervisor.Status.Instances).To(HaveLen(1))
Expect(hypervisor.Status.Instances[0].ID).To(Equal("25e2ea06-f6be-4bac-856d-8c2d0bdbcdee"))
- Expect(hypervisor.Status.Conditions).To(HaveLen(3))
+ Expect(hypervisor.Status.Conditions).To(HaveLen(2))
Expect(hypervisor.Status.Conditions[0].Type).To(Equal("test-unit"))
Expect(hypervisor.Status.Conditions[0].Status).To(Equal(metav1.ConditionTrue))
@@ -165,10 +158,6 @@ var _ = Describe("Hypervisor Controller", func() {
Expect(hypervisor.Status.Conditions[1].Status).To(Equal(metav1.ConditionTrue))
Expect(hypervisor.Status.Conditions[1].Reason).To(Equal("Connected"))
- Expect(hypervisor.Status.Conditions[2].Type).To(Equal("CapabilitiesClientConnection"))
- Expect(hypervisor.Status.Conditions[2].Status).To(Equal(metav1.ConditionTrue))
- Expect(hypervisor.Status.Conditions[2].Reason).To(Equal("CapabilitiesClientGetSucceeded"))
-
Expect(hypervisor.Status.Capabilities.HostCpuArch).To(Equal("x86_64"))
Expect(hypervisor.Status.Capabilities.HostCpus.AsDec().UnscaledBig()).
To(Equal(resource.NewQuantity(4, resource.DecimalSI).AsDec().UnscaledBig()))
diff --git a/internal/emulator/libvirt.go b/internal/emulator/libvirt.go
index 5dbaaad..3bbdbc9 100644
--- a/internal/emulator/libvirt.go
+++ b/internal/emulator/libvirt.go
@@ -21,11 +21,9 @@ import (
"context"
v1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
- golibvirt "github.com/digitalocean/go-libvirt"
logger "sigs.k8s.io/controller-runtime/pkg/log"
"github.com/cobaltcore-dev/kvm-node-agent/internal/libvirt"
- "github.com/cobaltcore-dev/kvm-node-agent/internal/libvirt/capabilities"
)
func NewLibVirtEmulator(ctx context.Context) *libvirt.InterfaceMock {
@@ -39,24 +37,9 @@ func NewLibVirtEmulator(ctx context.Context) *libvirt.InterfaceMock {
log.Info("Connect Func called")
return nil
},
- GetDomainsActiveFunc: func() ([]golibvirt.Domain, error) {
- log.Info("GetDomainsActiveFunc Func called")
- return []golibvirt.Domain{}, nil
- },
- GetInstancesFunc: func() ([]v1.Instance, error) {
- log.Info("GetInstancesFunc Func called")
- return nil, nil
- },
- GetVersionFunc: func() string {
- log.Info("GetVersionFunc Func called")
- return "10.9.0"
- },
- IsConnectedFunc: func() bool {
- log.Info("IsConnectedFunc Func called")
- return true
- },
- GetCapabilitiesFunc: func() (v1.CapabilitiesStatus, error) {
- return capabilities.NewClientEmulator().Get(nil)
+ ProcessFunc: func(hv v1.Hypervisor) (v1.Hypervisor, error) {
+ log.Info("Process Func called")
+ return hv, nil
},
}
return mockedInterface
diff --git a/internal/libvirt/capabilities/client.go b/internal/libvirt/capabilities/client.go
index 3180c9e..554400f 100644
--- a/internal/libvirt/capabilities/client.go
+++ b/internal/libvirt/capabilities/client.go
@@ -19,18 +19,15 @@ package capabilities
import (
"encoding/xml"
- "fmt"
- v1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
libvirt "github.com/digitalocean/go-libvirt"
- "k8s.io/apimachinery/pkg/api/resource"
"sigs.k8s.io/controller-runtime/pkg/log"
)
// Client that returns the capabilities of the host we are mounted on.
type Client interface {
// Return the capabilities status of the host we are mounted on.
- Get(virt *libvirt.Libvirt) (v1.CapabilitiesStatus, error)
+ Get(virt *libvirt.Libvirt) (Capabilities, error)
}
// Implementation of the CapabilitiesClient interface.
@@ -42,18 +39,18 @@ func NewClient() Client {
}
// Return the capabilities of the host we are mounted on.
-func (m *client) Get(virt *libvirt.Libvirt) (v1.CapabilitiesStatus, error) {
+func (m *client) Get(virt *libvirt.Libvirt) (Capabilities, error) {
capabilitiesXMLBytes, err := virt.Capabilities()
if err != nil {
log.Log.Error(err, "failed to get libvirt capabilities")
- return v1.CapabilitiesStatus{}, err
+ return Capabilities{}, err
}
var capabilities Capabilities
if err := xml.Unmarshal(capabilitiesXMLBytes, &capabilities); err != nil {
log.Log.Error(err, "failed to unmarshal libvirt capabilities")
- return v1.CapabilitiesStatus{}, err
+ return Capabilities{}, err
}
- return convert(capabilities)
+ return capabilities, nil
}
// Emulated capabilities client returning an embedded capabilities xml.
@@ -65,35 +62,11 @@ func NewClientEmulator() Client {
}
// Get the capabilities of the host we are mounted on.
-func (c *clientEmulator) Get(virt *libvirt.Libvirt) (v1.CapabilitiesStatus, error) {
+func (c *clientEmulator) Get(virt *libvirt.Libvirt) (Capabilities, error) {
var capabilities Capabilities
if err := xml.Unmarshal(exampleXML, &capabilities); err != nil {
log.Log.Error(err, "failed to unmarshal example capabilities")
- return v1.CapabilitiesStatus{}, err
+ return Capabilities{}, err
}
- return convert(capabilities)
-}
-
-// Convert the libvirt capabilities to the API format.
-func convert(in Capabilities) (out v1.CapabilitiesStatus, err error) {
- out.HostCpuArch = in.Host.CPU.Arch
- // Loop over all numa cells to get the total memory + vcpus.
- totalMemory := resource.NewQuantity(0, resource.BinarySI)
- totalCpus := resource.NewQuantity(0, resource.DecimalSI)
- for _, cell := range in.Host.Topology.CellSpec.Cells {
- mem, err := cell.Memory.AsQuantity()
- if err != nil {
- return v1.CapabilitiesStatus{}, err
- }
- totalMemory.Add(mem)
- cpu := resource.NewQuantity(cell.CPUs.Num, resource.DecimalSI)
- if cpu == nil {
- return v1.CapabilitiesStatus{},
- fmt.Errorf("invalid CPU count for cell %d", cell.ID)
- }
- totalCpus.Add(*cpu)
- }
- out.HostMemory = *totalMemory
- out.HostCpus = *totalCpus
- return out, nil
+ return capabilities, nil
}
diff --git a/internal/libvirt/capabilities/client_test.go b/internal/libvirt/capabilities/client_test.go
index d96df98..932e74b 100644
--- a/internal/libvirt/capabilities/client_test.go
+++ b/internal/libvirt/capabilities/client_test.go
@@ -20,27 +20,26 @@ package capabilities
import (
"encoding/xml"
"testing"
-
- "k8s.io/apimachinery/pkg/api/resource"
)
func TestNewClient(t *testing.T) {
- // Test with default socket path
client := NewClient()
if client == nil {
t.Fatal("NewClient() returned nil")
}
-}
-func TestNewClientWithCustomSocket(t *testing.T) {
- // Set custom socket path using t.Setenv for automatic cleanup
- customSocket := "/custom/libvirt-sock"
- t.Setenv("LIBVIRT_SOCKET", customSocket)
+ // Verify it implements the Client interface
+ var _ = client
+}
+func TestNewClient_ReturnsCorrectType(t *testing.T) {
client := NewClient()
+ // Verify it returns a non-nil implementation
if client == nil {
- t.Fatal("NewClient() returned nil with custom socket")
+ t.Fatal("NewClient() returned nil")
}
+ // Verify it's not the emulator type by checking behavior
+ // (we can't check the unexported type directly)
}
func TestNewClientEmulator(t *testing.T) {
@@ -48,415 +47,332 @@ func TestNewClientEmulator(t *testing.T) {
if client == nil {
t.Fatal("NewClientEmulator() returned nil")
}
+
+ // Verify it implements the Client interface
+ var _ = client
}
-func TestClientEmulatorGet(t *testing.T) {
+func TestNewClientEmulator_ReturnsCorrectType(t *testing.T) {
client := NewClientEmulator()
+ // Verify it returns a non-nil implementation
+ if client == nil {
+ t.Fatal("NewClientEmulator() returned nil")
+ }
+ // Verify it works without a libvirt connection (emulator behavior)
+ _, err := client.Get(nil)
+ if err != nil {
+ t.Errorf("Emulator should work with nil libvirt connection, got error: %v", err)
+ }
+}
+
+func TestClientEmulator_Get_Success(t *testing.T) {
+ client := NewClientEmulator()
+
+ // The emulator doesn't actually use the libvirt connection,
+ // so we pass nil to test it doesn't panic
+ capabilities, err := client.Get(nil)
- status, err := client.Get(nil)
if err != nil {
- t.Fatalf("clientEmulator.Get() returned error: %v", err)
+ t.Fatalf("Get() returned unexpected error: %v", err)
}
- // Verify the returned status has expected values based on example XML
- if status.HostCpuArch != "x86_64" {
- t.Errorf("Expected HostCpuArch to be 'x86_64', got '%s'", status.HostCpuArch)
+ // Verify the returned capabilities has expected structure
+ if capabilities.Host.CPU.Arch == "" {
+ t.Error("Expected capabilities to have Host CPU architecture")
}
+}
- // Verify memory is calculated correctly (sum of all NUMA cells)
- // From the example XML, we have 4 cells with different memory values:
- // Cell 0: 1056462864 KiB, Cell 1: 1056946772 KiB, Cell 2: 1056946772 KiB, Cell 3: 1056932756 KiB
- expectedMemoryKiB := int64(1056462864 + 1056946772 + 1056946772 + 1056932756)
- expectedMemoryBytes := expectedMemoryKiB * 1024 // Convert KiB to bytes
- expectedMemory := resource.NewQuantity(expectedMemoryBytes, resource.BinarySI)
+func TestClientEmulator_Get_ValidXML(t *testing.T) {
+ client := NewClientEmulator()
+ capabilities, err := client.Get(nil)
- if !status.HostMemory.Equal(*expectedMemory) {
- t.Errorf("Expected HostMemory to be %s, got %s", expectedMemory.String(), status.HostMemory.String())
+ if err != nil {
+ t.Fatalf("Get() returned unexpected error: %v", err)
}
- // Verify CPUs are calculated correctly (sum of all NUMA cells)
- // From the example XML, we have 4 cells with 64 CPUs each
- expectedCpus := resource.NewQuantity(4*64, resource.DecimalSI)
+ // Verify the embedded XML can be parsed correctly
+ var testCaps Capabilities
+ if err := xml.Unmarshal(exampleXML, &testCaps); err != nil {
+ t.Fatalf("Failed to unmarshal example XML: %v", err)
+ }
- if !status.HostCpus.Equal(*expectedCpus) {
- t.Errorf("Expected HostCpus to be %s, got %s", expectedCpus.String(), status.HostCpus.String())
+ // The emulator should return the same data
+ if capabilities.Host.CPU.Arch != testCaps.Host.CPU.Arch {
+ t.Errorf("Expected host CPU arch '%s', got '%s'",
+ testCaps.Host.CPU.Arch, capabilities.Host.CPU.Arch)
}
}
-func TestConvert(t *testing.T) {
- // Create test capabilities data
- capabilities := Capabilities{
- Host: CapabilitiesHost{
- CPU: CapabilitiesHostCPU{
- Arch: "x86_64",
- },
- Topology: CapabilitiesHostTopology{
- CellSpec: CapabilitiesHostTopologyCells{
- Num: 2,
- Cells: []CapabilitiesHostTopologyCell{
- {
- ID: 0,
- Memory: CapabilitiesHostTopologyCellMemory{
- Unit: "KiB",
- Value: 1024000, // 1GB in KiB
- },
- CPUs: CapabilitiesHostTopologyCellCPUs{
- Num: 4,
- },
- },
- {
- ID: 1,
- Memory: CapabilitiesHostTopologyCellMemory{
- Unit: "KiB",
- Value: 2048000, // 2GB in KiB
- },
- CPUs: CapabilitiesHostTopologyCellCPUs{
- Num: 8,
- },
- },
- },
- },
- },
- },
+func TestClientEmulator_Get_Consistency(t *testing.T) {
+ client := NewClientEmulator()
+
+ // Call Get multiple times and verify consistent results
+ result1, err1 := client.Get(nil)
+ if err1 != nil {
+ t.Fatalf("First Get() call failed: %v", err1)
}
- status, err := convert(capabilities)
+ result2, err2 := client.Get(nil)
+ if err2 != nil {
+ t.Fatalf("Second Get() call failed: %v", err2)
+ }
+
+ // Compare Host CPU architecture
+ if result1.Host.CPU.Arch != result2.Host.CPU.Arch {
+ t.Errorf("Inconsistent host CPU arch: '%s' vs '%s'",
+ result1.Host.CPU.Arch, result2.Host.CPU.Arch)
+ }
+}
+
+func TestClientEmulator_Get_HostInfo(t *testing.T) {
+ client := NewClientEmulator()
+ capabilities, err := client.Get(nil)
+
if err != nil {
- t.Fatalf("convert() returned error: %v", err)
+ t.Fatalf("Get() returned unexpected error: %v", err)
}
- // Verify CPU architecture
- if status.HostCpuArch != "x86_64" {
- t.Errorf("Expected HostCpuArch to be 'x86_64', got '%s'", status.HostCpuArch)
+ // Verify basic host fields
+ if capabilities.Host.CPU.Arch == "" {
+ t.Error("Expected host CPU architecture to be set")
}
+}
- // Verify total memory (1GB + 2GB = 3GB)
- expectedMemoryBytes := int64((1024000 + 2048000) * 1024) // Convert KiB to bytes
- expectedMemory := resource.NewQuantity(expectedMemoryBytes, resource.BinarySI)
+func TestClientEmulator_Get_CPUInfo(t *testing.T) {
+ client := NewClientEmulator()
+ capabilities, err := client.Get(nil)
- if !status.HostMemory.Equal(*expectedMemory) {
- t.Errorf("Expected HostMemory to be %s, got %s", expectedMemory.String(), status.HostMemory.String())
+ if err != nil {
+ t.Fatalf("Get() returned unexpected error: %v", err)
}
- // Verify total CPUs (4 + 8 = 12)
- expectedCpus := resource.NewQuantity(12, resource.DecimalSI)
+ cpu := capabilities.Host.CPU
- if !status.HostCpus.Equal(*expectedCpus) {
- t.Errorf("Expected HostCpus to be %s, got %s", expectedCpus.String(), status.HostCpus.String())
+ // Verify CPU information
+ if cpu.Arch == "" {
+ t.Error("Expected CPU architecture to be set")
}
}
-func TestConvertWithDifferentMemoryUnits(t *testing.T) {
- testCases := []struct {
- name string
- unit string
- value int64
- expectedBytes int64
- }{
- {
- name: "KiB unit",
- unit: "KiB",
- value: 1024,
- expectedBytes: 1024 * 1024,
- },
- {
- name: "MiB unit",
- unit: "MiB",
- value: 1,
- expectedBytes: 1024 * 1024,
- },
- {
- name: "GiB unit",
- unit: "GiB",
- value: 1,
- expectedBytes: 1024 * 1024 * 1024,
- },
+func TestClientEmulator_Get_TopologyInfo(t *testing.T) {
+ client := NewClientEmulator()
+ capabilities, err := client.Get(nil)
+
+ if err != nil {
+ t.Fatalf("Get() returned unexpected error: %v", err)
}
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- capabilities := Capabilities{
- Host: CapabilitiesHost{
- CPU: CapabilitiesHostCPU{
- Arch: "x86_64",
- },
- Topology: CapabilitiesHostTopology{
- CellSpec: CapabilitiesHostTopologyCells{
- Num: 1,
- Cells: []CapabilitiesHostTopologyCell{
- {
- ID: 0,
- Memory: CapabilitiesHostTopologyCellMemory{
- Unit: tc.unit,
- Value: tc.value,
- },
- CPUs: CapabilitiesHostTopologyCellCPUs{
- Num: 1,
- },
- },
- },
- },
- },
- },
- }
+ topology := capabilities.Host.Topology
- status, err := convert(capabilities)
- if err != nil {
- t.Fatalf("convert() returned error: %v", err)
- }
+ // Verify topology information is accessible
+ if topology.CellSpec.Num <= 0 {
+ t.Skip("No NUMA cells found in example XML")
+ }
- expectedMemory := resource.NewQuantity(tc.expectedBytes, resource.BinarySI)
- if !status.HostMemory.Equal(*expectedMemory) {
- t.Errorf("Expected HostMemory to be %s, got %s", expectedMemory.String(), status.HostMemory.String())
- }
- })
+ if len(topology.CellSpec.Cells) == 0 {
+ t.Error("Expected at least one NUMA cell when num > 0")
}
}
-func TestConvertWithInvalidMemoryUnit(t *testing.T) {
- capabilities := Capabilities{
- Host: CapabilitiesHost{
- CPU: CapabilitiesHostCPU{
- Arch: "x86_64",
- },
- Topology: CapabilitiesHostTopology{
- CellSpec: CapabilitiesHostTopologyCells{
- Num: 1,
- Cells: []CapabilitiesHostTopologyCell{
- {
- ID: 0,
- Memory: CapabilitiesHostTopologyCellMemory{
- Unit: "InvalidUnit",
- Value: 1024,
- },
- CPUs: CapabilitiesHostTopologyCellCPUs{
- Num: 1,
- },
- },
- },
- },
- },
- },
+func TestClientEmulator_Get_GuestInfo(t *testing.T) {
+ client := NewClientEmulator()
+ capabilities, err := client.Get(nil)
+
+ if err != nil {
+ t.Fatalf("Get() returned unexpected error: %v", err)
}
- _, err := convert(capabilities)
- if err == nil {
- t.Error("Expected convert() to return error for invalid memory unit, but got nil")
+ guest := capabilities.Guest
+
+ // Verify guest fields are accessible
+ if guest.OSType == "" {
+ t.Error("Expected guest OS type to be set")
}
- expectedError := "unknown memory unit InvalidUnit"
- if err.Error() != expectedError {
- t.Errorf("Expected error message '%s', got '%s'", expectedError, err.Error())
+ if guest.Arch.Name == "" {
+ t.Error("Expected guest architecture name to be set")
}
}
-func TestConvertWithZeroCPUs(t *testing.T) {
- capabilities := Capabilities{
- Host: CapabilitiesHost{
- CPU: CapabilitiesHostCPU{
- Arch: "x86_64",
- },
- Topology: CapabilitiesHostTopology{
- CellSpec: CapabilitiesHostTopologyCells{
- Num: 1,
- Cells: []CapabilitiesHostTopologyCell{
- {
- ID: 0,
- Memory: CapabilitiesHostTopologyCellMemory{
- Unit: "KiB",
- Value: 1024,
- },
- CPUs: CapabilitiesHostTopologyCellCPUs{
- Num: 0,
- },
- },
- },
- },
- },
- },
- }
+func TestClient_InterfaceCompliance(t *testing.T) {
+ // Ensure both implementations satisfy the Client interface
+ var _ = NewClient()
+ var _ = NewClientEmulator()
+}
- status, err := convert(capabilities)
- if err != nil {
- t.Fatalf("convert() returned unexpected error: %v", err)
+func TestExampleXML_IsValid(t *testing.T) {
+ // Verify that the embedded example XML can be parsed
+ var capabilities Capabilities
+ if err := xml.Unmarshal(exampleXML, &capabilities); err != nil {
+ t.Fatalf("Failed to unmarshal example XML: %v", err)
}
- expectedCpus := resource.NewQuantity(0, resource.DecimalSI)
- if !status.HostCpus.Equal(*expectedCpus) {
- t.Errorf("Expected HostCpus to be %s, got %s", expectedCpus.String(), status.HostCpus.String())
+ // Basic validation that we got some data
+ if capabilities.Host.CPU.Arch == "" {
+ t.Error("Expected CPU architecture to be present in example XML")
}
}
-func TestConvertWithEmptyCells(t *testing.T) {
- capabilities := Capabilities{
- Host: CapabilitiesHost{
- CPU: CapabilitiesHostCPU{
- Arch: "aarch64",
- },
- Topology: CapabilitiesHostTopology{
- CellSpec: CapabilitiesHostTopologyCells{
- Num: 0,
- Cells: []CapabilitiesHostTopologyCell{},
- },
- },
+func TestExampleXML_Structure(t *testing.T) {
+ var capabilities Capabilities
+ if err := xml.Unmarshal(exampleXML, &capabilities); err != nil {
+ t.Fatalf("Failed to unmarshal example XML: %v", err)
+ }
+
+ // Test that the example XML has reasonable structure
+ tests := []struct {
+ name string
+ checkFunc func() bool
+ errMsg string
+ }{
+ {
+ name: "Has CPU Arch",
+ checkFunc: func() bool { return capabilities.Host.CPU.Arch != "" },
+ errMsg: "Expected Host CPU Arch to be populated",
+ },
+ {
+ name: "Has Guest OSType",
+ checkFunc: func() bool { return capabilities.Guest.OSType != "" },
+ errMsg: "Expected Guest OSType to be populated",
+ },
+ {
+ name: "Has Guest Arch Name",
+ checkFunc: func() bool { return capabilities.Guest.Arch.Name != "" },
+ errMsg: "Expected Guest Arch Name to be populated",
},
}
- status, err := convert(capabilities)
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if !tt.checkFunc() {
+ t.Error(tt.errMsg)
+ }
+ })
+ }
+}
+
+func TestClientEmulator_Get_ReturnsStruct(t *testing.T) {
+ client := NewClientEmulator()
+ result, err := client.Get(nil)
+
if err != nil {
- t.Fatalf("convert() returned unexpected error: %v", err)
+ t.Fatalf("Get() returned unexpected error: %v", err)
}
- // Verify CPU architecture is preserved
- if status.HostCpuArch != "aarch64" {
- t.Errorf("Expected HostCpuArch to be 'aarch64', got '%s'", status.HostCpuArch)
+ // Verify the result has expected data
+ if result.Host.CPU.Arch == "" && result.Guest.OSType == "" {
+ t.Error("Expected result to have either Host or Guest populated")
}
+}
+
+func TestClientTypes_AreDistinct(t *testing.T) {
+ client1 := NewClient()
+ client2 := NewClientEmulator()
- // Verify zero memory and CPUs
- expectedMemory := resource.NewQuantity(0, resource.BinarySI)
- expectedCpus := resource.NewQuantity(0, resource.DecimalSI)
+ // Verify they are different types
+ type1 := any(client1)
+ type2 := any(client2)
- if !status.HostMemory.Equal(*expectedMemory) {
- t.Errorf("Expected HostMemory to be %s, got %s", expectedMemory.String(), status.HostMemory.String())
+ if type1 == type2 {
+ t.Error("Expected NewClient() and NewClientEmulator() to return different types")
}
+}
- if !status.HostCpus.Equal(*expectedCpus) {
- t.Errorf("Expected HostCpus to be %s, got %s", expectedCpus.String(), status.HostCpus.String())
+func TestExampleXML_IOMMUSupport(t *testing.T) {
+ var capabilities Capabilities
+ if err := xml.Unmarshal(exampleXML, &capabilities); err != nil {
+ t.Fatalf("Failed to unmarshal example XML: %v", err)
}
+
+ // IOMMU support should be accessible
+ // This test just verifies we can access it without panic
+ _ = capabilities.Host.IOMMU.Support
}
-func TestConvertWithRealExampleData(t *testing.T) {
- // Use the embedded example XML to test conversion
+func TestExampleXML_CacheInfo(t *testing.T) {
var capabilities Capabilities
- err := xml.Unmarshal(exampleXML, &capabilities)
- if err != nil {
+ if err := xml.Unmarshal(exampleXML, &capabilities); err != nil {
t.Fatalf("Failed to unmarshal example XML: %v", err)
}
- status, err := convert(capabilities)
+ // Cache banks should be accessible
+ // This test just verifies we can access them without panic
+ _ = capabilities.Host.Cache.Banks
+}
+
+func TestClientEmulator_Get_NoPanic(t *testing.T) {
+ client := NewClientEmulator()
+
+ // Ensure calling Get doesn't panic even with nil libvirt
+ defer func() {
+ if r := recover(); r != nil {
+ t.Errorf("Get() panicked with nil libvirt: %v", r)
+ }
+ }()
+
+ _, err := client.Get(nil)
if err != nil {
- t.Fatalf("convert() returned error with real example data: %v", err)
+ t.Fatalf("Get() returned unexpected error: %v", err)
}
+}
- // Verify the status is valid
- if status.HostCpuArch == "" {
- t.Error("HostCpuArch should not be empty")
+func TestExampleXML_TopologyCells(t *testing.T) {
+ var capabilities Capabilities
+ if err := xml.Unmarshal(exampleXML, &capabilities); err != nil {
+ t.Fatalf("Failed to unmarshal example XML: %v", err)
}
- // Memory and CPU quantities should be positive
- if status.HostMemory.IsZero() {
- t.Error("HostMemory should not be zero")
- }
+ // If we have NUMA cells, verify their structure
+ if capabilities.Host.Topology.CellSpec.Num > 0 {
+ if len(capabilities.Host.Topology.CellSpec.Cells) == 0 {
+ t.Error("Expected cells to be present when num > 0")
+ }
- if status.HostCpus.IsZero() {
- t.Error("HostCpus should not be zero")
- }
+ // Verify we can access cell properties
+ for i, cell := range capabilities.Host.Topology.CellSpec.Cells {
+ _ = cell.ID
+ _ = cell.Memory.Value
+ _ = cell.CPUs.Num
- // Verify specific values from example XML
- if status.HostCpuArch != "x86_64" {
- t.Errorf("Expected HostCpuArch to be 'x86_64', got '%s'", status.HostCpuArch)
+ // Just basic validation on first cell
+ if i == 0 && cell.Memory.Value <= 0 {
+ t.Error("Expected positive memory value for first cell")
+ }
+ }
}
}
-// Test helper function to create a mock capabilities structure
-func createMockCapabilities(arch string, cells []mockCell) Capabilities {
- capabilitiesCells := make([]CapabilitiesHostTopologyCell, len(cells))
- for i, cell := range cells {
- capabilitiesCells[i] = CapabilitiesHostTopologyCell{
- ID: cell.ID,
- Memory: CapabilitiesHostTopologyCellMemory{
- Unit: cell.MemoryUnit,
- Value: cell.MemoryValue,
- },
- CPUs: CapabilitiesHostTopologyCellCPUs{
- Num: cell.CPUCount,
- },
- }
+func TestExampleXML_GuestArchDomain(t *testing.T) {
+ var capabilities Capabilities
+ if err := xml.Unmarshal(exampleXML, &capabilities); err != nil {
+ t.Fatalf("Failed to unmarshal example XML: %v", err)
}
- return Capabilities{
- Host: CapabilitiesHost{
- CPU: CapabilitiesHostCPU{
- Arch: arch,
- },
- Topology: CapabilitiesHostTopology{
- CellSpec: CapabilitiesHostTopologyCells{
- Num: len(cells),
- Cells: capabilitiesCells,
- },
- },
- },
+ guest := capabilities.Guest
+
+ // Verify guest arch domain is accessible
+ if guest.Arch.Domain.Type == "" {
+ t.Skip("Guest arch domain type not set in example XML")
}
-}
-type mockCell struct {
- ID int
- MemoryUnit string
- MemoryValue int64
- CPUCount int64
+ // Domain type should be something like "kvm" or "qemu"
+ _ = guest.Arch.Domain.Type
}
-func TestConvertWithMultipleCellsAndArchitectures(t *testing.T) {
- testCases := []struct {
- name string
- arch string
- cells []mockCell
- expectedMemory int64 // in bytes
- expectedCPUs int64
- }{
- {
- name: "Single cell x86_64",
- arch: "x86_64",
- cells: []mockCell{
- {ID: 0, MemoryUnit: "KiB", MemoryValue: 1024, CPUCount: 2},
- },
- expectedMemory: 1024 * 1024,
- expectedCPUs: 2,
- },
- {
- name: "Multiple cells aarch64",
- arch: "aarch64",
- cells: []mockCell{
- {ID: 0, MemoryUnit: "MiB", MemoryValue: 512, CPUCount: 4},
- {ID: 1, MemoryUnit: "MiB", MemoryValue: 1024, CPUCount: 8},
- },
- expectedMemory: (512 + 1024) * 1024 * 1024,
- expectedCPUs: 12,
- },
- {
- name: "Mixed memory units",
- arch: "ppc64le",
- cells: []mockCell{
- {ID: 0, MemoryUnit: "KiB", MemoryValue: 1048576, CPUCount: 1}, // 1GB in KiB
- {ID: 1, MemoryUnit: "MiB", MemoryValue: 1024, CPUCount: 2}, // 1GB in MiB
- },
- expectedMemory: 2 * 1024 * 1024 * 1024, // 2GB total
- expectedCPUs: 3,
- },
+func TestExampleXML_WordSize(t *testing.T) {
+ var capabilities Capabilities
+ if err := xml.Unmarshal(exampleXML, &capabilities); err != nil {
+ t.Fatalf("Failed to unmarshal example XML: %v", err)
}
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- capabilities := createMockCapabilities(tc.arch, tc.cells)
-
- status, err := convert(capabilities)
- if err != nil {
- t.Fatalf("convert() returned error: %v", err)
- }
-
- if status.HostCpuArch != tc.arch {
- t.Errorf("Expected HostCpuArch to be '%s', got '%s'", tc.arch, status.HostCpuArch)
- }
-
- expectedMemory := resource.NewQuantity(tc.expectedMemory, resource.BinarySI)
- if !status.HostMemory.Equal(*expectedMemory) {
- t.Errorf("Expected HostMemory to be %s, got %s", expectedMemory.String(), status.HostMemory.String())
- }
+ // Word size should be present (typically 32 or 64)
+ if capabilities.Guest.Arch.WordSize <= 0 {
+ t.Error("Expected positive word size value")
+ }
- expectedCpus := resource.NewQuantity(tc.expectedCPUs, resource.DecimalSI)
- if !status.HostCpus.Equal(*expectedCpus) {
- t.Errorf("Expected HostCpus to be %s, got %s", expectedCpus.String(), status.HostCpus.String())
- }
- })
+ // Typical word sizes are 32 or 64
+ wordSize := capabilities.Guest.Arch.WordSize
+ if wordSize != 32 && wordSize != 64 {
+ t.Logf("Note: Unusual word size %d (expected 32 or 64)", wordSize)
}
}
diff --git a/internal/libvirt/capabilities/schema.go b/internal/libvirt/capabilities/schema.go
index a911bf6..bc7897b 100644
--- a/internal/libvirt/capabilities/schema.go
+++ b/internal/libvirt/capabilities/schema.go
@@ -17,12 +17,6 @@ limitations under the License.
package capabilities
-import (
- "fmt"
-
- "k8s.io/apimachinery/pkg/api/resource"
-)
-
// Capabilities as returned from the libvirt driver capabilities api.
//
// The format is the same as returned when executing `virsh capabilities`. See:
@@ -59,7 +53,7 @@ type CapabilitiesHostTopologyCells struct {
}
type CapabilitiesHostTopologyCell struct {
- ID int `xml:"id,attr"`
+ ID uint64 `xml:"id,attr"`
Memory CapabilitiesHostTopologyCellMemory `xml:"memory"`
Pages []CapabilitiesHostTopologyCellPages `xml:"pages"`
Distances CapabilitiesHostTopologyCellDistances `xml:"distances"`
@@ -71,27 +65,6 @@ type CapabilitiesHostTopologyCellMemory struct {
Value int64 `xml:",chardata"`
}
-// Get the cell memory as resource.Quantity.
-func (c CapabilitiesHostTopologyCellMemory) AsQuantity() (resource.Quantity, error) {
- var quantity *resource.Quantity
- // Check the unit
- switch c.Unit {
- case "KiB":
- quantity = resource.NewQuantity(c.Value*1024, resource.BinarySI)
- case "MiB":
- quantity = resource.NewQuantity(c.Value*1024*1024, resource.BinarySI)
- case "GiB":
- quantity = resource.NewQuantity(c.Value*1024*1024*1024, resource.BinarySI)
- case "TiB":
- quantity = resource.NewQuantity(c.Value*1024*1024*1024*1024, resource.BinarySI)
- }
- if quantity == nil {
- return resource.Quantity{}, fmt.Errorf("unknown memory unit %s", c.Unit)
- }
- // Set the value
- return *quantity, nil
-}
-
type CapabilitiesHostTopologyCellPages struct {
Unit string `xml:"unit,attr"`
Size int `xml:"size,attr"`
diff --git a/internal/libvirt/domcapabilities/client.go b/internal/libvirt/domcapabilities/client.go
new file mode 100644
index 0000000..9add217
--- /dev/null
+++ b/internal/libvirt/domcapabilities/client.go
@@ -0,0 +1,74 @@
+/*
+SPDX-FileCopyrightText: Copyright 2025 SAP SE or an SAP affiliate company and cobaltcore-dev contributors
+SPDX-License-Identifier: Apache-2.0
+
+Licensed under the Apache License, LibVirtVersion 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package domcapabilities
+
+import (
+ "encoding/xml"
+
+ libvirt "github.com/digitalocean/go-libvirt"
+ "sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+// Client that returns the domain capabilities of the host we are mounted on.
+type Client interface {
+ // Return the capabilities status of the host we are mounted on.
+ Get(virt *libvirt.Libvirt) (DomainCapabilities, error)
+}
+
+// Implementation of the Client interface.
+type client struct{}
+
+// Create a new domain capabilities client.
+func NewClient() Client {
+ return &client{}
+}
+
+// Return the domain capabilities of the host we are mounted on.
+func (m *client) Get(virt *libvirt.Libvirt) (DomainCapabilities, error) {
+ // Same as running `virsh domcapabilities` without any arguments.
+ capabilitiesXMLStr, err := virt.
+ ConnectGetDomainCapabilities(nil, nil, nil, nil, 0)
+ if err != nil {
+ log.Log.Error(err, "failed to get libvirt capabilities")
+ return DomainCapabilities{}, err
+ }
+ var capabilities DomainCapabilities
+ if err := xml.Unmarshal([]byte(capabilitiesXMLStr), &capabilities); err != nil {
+ log.Log.Error(err, "failed to unmarshal libvirt capabilities")
+ return DomainCapabilities{}, err
+ }
+ return capabilities, nil
+}
+
+// Emulated domain capabilities client returning an embedded capabilities xml.
+type clientEmulator struct{}
+
+// Create a new emulated domain capabilities client.
+func NewClientEmulator() Client {
+ return &clientEmulator{}
+}
+
+// Get the domain capabilities of the host we are mounted on.
+func (c *clientEmulator) Get(virt *libvirt.Libvirt) (DomainCapabilities, error) {
+ var capabilities DomainCapabilities
+ if err := xml.Unmarshal(exampleXML, &capabilities); err != nil {
+ log.Log.Error(err, "failed to unmarshal example capabilities")
+ return DomainCapabilities{}, err
+ }
+ return capabilities, nil
+}
diff --git a/internal/libvirt/domcapabilities/client_test.go b/internal/libvirt/domcapabilities/client_test.go
new file mode 100644
index 0000000..fc11881
--- /dev/null
+++ b/internal/libvirt/domcapabilities/client_test.go
@@ -0,0 +1,485 @@
+/*
+SPDX-FileCopyrightText: Copyright 2025 SAP SE or an SAP affiliate company and cobaltcore-dev contributors
+SPDX-License-Identifier: Apache-2.0
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package domcapabilities
+
+import (
+ "encoding/xml"
+ "testing"
+)
+
+func TestNewClient(t *testing.T) {
+ client := NewClient()
+ if client == nil {
+ t.Fatal("NewClient() returned nil")
+ }
+
+ // Verify it implements the Client interface
+ var _ = client
+}
+
+func TestNewClient_ReturnsCorrectType(t *testing.T) {
+ client := NewClient()
+ // Verify it returns a non-nil implementation
+ if client == nil {
+ t.Fatal("NewClient() returned nil")
+ }
+ // Verify it's not the emulator type by checking behavior
+ // (we can't check the unexported type directly)
+}
+
+func TestNewClientEmulator(t *testing.T) {
+ client := NewClientEmulator()
+ if client == nil {
+ t.Fatal("NewClientEmulator() returned nil")
+ }
+
+ // Verify it implements the Client interface
+ var _ = client
+}
+
+func TestNewClientEmulator_ReturnsCorrectType(t *testing.T) {
+ client := NewClientEmulator()
+ // Verify it returns a non-nil implementation
+ if client == nil {
+ t.Fatal("NewClientEmulator() returned nil")
+ }
+ // Verify it works without a libvirt connection (emulator behavior)
+ _, err := client.Get(nil)
+ if err != nil {
+ t.Errorf("Emulator should work with nil libvirt connection, got error: %v", err)
+ }
+}
+
+func TestClientEmulator_Get_Success(t *testing.T) {
+ client := NewClientEmulator()
+
+ // The emulator doesn't actually use the libvirt connection,
+ // so we pass nil to test it doesn't panic
+ capabilities, err := client.Get(nil)
+
+ if err != nil {
+ t.Fatalf("Get() returned unexpected error: %v", err)
+ }
+
+ // Verify the returned capabilities has expected structure
+ if capabilities.Domain == "" {
+ t.Error("Expected capabilities to have Domain type")
+ }
+
+ if capabilities.Arch == "" {
+ t.Error("Expected capabilities to have Arch")
+ }
+}
+
+func TestClientEmulator_Get_ValidXML(t *testing.T) {
+ client := NewClientEmulator()
+ capabilities, err := client.Get(nil)
+
+ if err != nil {
+ t.Fatalf("Get() returned unexpected error: %v", err)
+ }
+
+ // Verify the embedded XML can be parsed correctly
+ var testCaps DomainCapabilities
+ if err := xml.Unmarshal(exampleXML, &testCaps); err != nil {
+ t.Fatalf("Failed to unmarshal example XML: %v", err)
+ }
+
+ // The emulator should return the same data
+ if capabilities.Domain != testCaps.Domain {
+ t.Errorf("Expected domain type '%s', got '%s'", testCaps.Domain, capabilities.Domain)
+ }
+
+ if capabilities.Arch != testCaps.Arch {
+ t.Errorf("Expected arch '%s', got '%s'", testCaps.Arch, capabilities.Arch)
+ }
+}
+
+func TestClientEmulator_Get_Consistency(t *testing.T) {
+ client := NewClientEmulator()
+
+ // Call Get multiple times and verify consistent results
+ result1, err1 := client.Get(nil)
+ if err1 != nil {
+ t.Fatalf("First Get() call failed: %v", err1)
+ }
+
+ result2, err2 := client.Get(nil)
+ if err2 != nil {
+ t.Fatalf("Second Get() call failed: %v", err2)
+ }
+
+ // Compare domain types
+ if result1.Domain != result2.Domain {
+ t.Errorf("Inconsistent domain types: '%s' vs '%s'", result1.Domain, result2.Domain)
+ }
+
+ // Compare architectures
+ if result1.Arch != result2.Arch {
+ t.Errorf("Inconsistent architectures: '%s' vs '%s'", result1.Arch, result2.Arch)
+ }
+}
+
+func TestClientEmulator_Get_OSInfo(t *testing.T) {
+ client := NewClientEmulator()
+ capabilities, err := client.Get(nil)
+
+ if err != nil {
+ t.Fatalf("Get() returned unexpected error: %v", err)
+ }
+
+ // Verify OS capabilities are accessible
+ if capabilities.OS.Supported == "" {
+ t.Skip("OS supported attribute not set in example XML")
+ }
+
+ // Loader information should be accessible
+ _ = capabilities.OS.Loader.Supported
+}
+
+func TestClientEmulator_Get_CPUInfo(t *testing.T) {
+ client := NewClientEmulator()
+ capabilities, err := client.Get(nil)
+
+ if err != nil {
+ t.Fatalf("Get() returned unexpected error: %v", err)
+ }
+
+ // Verify CPU modes are accessible
+ if len(capabilities.CPU.Modes) == 0 {
+ t.Skip("No CPU modes found in example XML")
+ }
+
+ // Verify we can access mode properties
+ mode := capabilities.CPU.Modes[0]
+ if mode.Name == "" {
+ t.Error("Expected CPU mode to have a name")
+ }
+}
+
+func TestClientEmulator_Get_DevicesInfo(t *testing.T) {
+ client := NewClientEmulator()
+ capabilities, err := client.Get(nil)
+
+ if err != nil {
+ t.Fatalf("Get() returned unexpected error: %v", err)
+ }
+
+ // Verify devices are accessible
+ if len(capabilities.Devices.Devices) == 0 {
+ t.Skip("No devices found in example XML")
+ }
+
+ // Verify we can access device properties
+ for _, device := range capabilities.Devices.Devices {
+ _ = device.Supported
+ _ = device.Enums
+ }
+}
+
+func TestClientEmulator_Get_FeaturesInfo(t *testing.T) {
+ client := NewClientEmulator()
+ capabilities, err := client.Get(nil)
+
+ if err != nil {
+ t.Fatalf("Get() returned unexpected error: %v", err)
+ }
+
+ // Verify features are accessible
+ if len(capabilities.Features.Features) == 0 {
+ t.Skip("No features found in example XML")
+ }
+
+ // Verify we can access feature properties
+ for _, feature := range capabilities.Features.Features {
+ _ = feature.Supported
+ }
+}
+
+func TestClient_InterfaceCompliance(t *testing.T) {
+ // Ensure both implementations satisfy the Client interface
+ var _ = NewClient()
+ var _ = NewClientEmulator()
+}
+
+func TestExampleXML_IsValid(t *testing.T) {
+ // Verify that the embedded example XML can be parsed
+ var capabilities DomainCapabilities
+ if err := xml.Unmarshal(exampleXML, &capabilities); err != nil {
+ t.Fatalf("Failed to unmarshal example XML: %v", err)
+ }
+
+ // Basic validation that we got some data
+ if capabilities.Domain == "" {
+ t.Error("Expected domain type to be present in example XML")
+ }
+
+ if capabilities.Arch == "" {
+ t.Error("Expected architecture to be present in example XML")
+ }
+}
+
+func TestExampleXML_Structure(t *testing.T) {
+ var capabilities DomainCapabilities
+ if err := xml.Unmarshal(exampleXML, &capabilities); err != nil {
+ t.Fatalf("Failed to unmarshal example XML: %v", err)
+ }
+
+ // Test that the example XML has reasonable structure
+ tests := []struct {
+ name string
+ checkFunc func() bool
+ errMsg string
+ }{
+ {
+ name: "Has Domain Type",
+ checkFunc: func() bool { return capabilities.Domain != "" },
+ errMsg: "Expected Domain type to be populated",
+ },
+ {
+ name: "Has Architecture",
+ checkFunc: func() bool { return capabilities.Arch != "" },
+ errMsg: "Expected Architecture to be populated",
+ },
+ {
+ name: "Has CPU Modes",
+ checkFunc: func() bool { return len(capabilities.CPU.Modes) > 0 },
+ errMsg: "Expected at least one CPU mode to be populated",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if !tt.checkFunc() {
+ t.Error(tt.errMsg)
+ }
+ })
+ }
+}
+
+func TestClientEmulator_Get_ReturnsStruct(t *testing.T) {
+ client := NewClientEmulator()
+ result, err := client.Get(nil)
+
+ if err != nil {
+ t.Fatalf("Get() returned unexpected error: %v", err)
+ }
+
+ // Verify the result has expected data
+ if result.Domain == "" && result.Arch == "" {
+ t.Error("Expected result to have either Domain or Arch populated")
+ }
+}
+
+func TestClientTypes_AreDistinct(t *testing.T) {
+ client1 := NewClient()
+ client2 := NewClientEmulator()
+
+ // Verify they are different types
+ type1 := any(client1)
+ type2 := any(client2)
+
+ if type1 == type2 {
+ t.Error("Expected NewClient() and NewClientEmulator() to return different types")
+ }
+}
+
+func TestExampleXML_CPUModes(t *testing.T) {
+ var capabilities DomainCapabilities
+ if err := xml.Unmarshal(exampleXML, &capabilities); err != nil {
+ t.Fatalf("Failed to unmarshal example XML: %v", err)
+ }
+
+ if len(capabilities.CPU.Modes) == 0 {
+ t.Skip("No CPU modes in example XML")
+ }
+
+ // Verify CPU mode properties
+ for _, mode := range capabilities.CPU.Modes {
+ if mode.Name == "" {
+ t.Error("Expected CPU mode to have a name")
+ }
+
+ // Supported attribute should be present
+ if mode.Supported == "" {
+ t.Logf("Note: CPU mode '%s' has no supported attribute", mode.Name)
+ }
+
+ // Enums should be accessible
+ _ = mode.Enums
+ }
+}
+
+func TestExampleXML_OSLoader(t *testing.T) {
+ var capabilities DomainCapabilities
+ if err := xml.Unmarshal(exampleXML, &capabilities); err != nil {
+ t.Fatalf("Failed to unmarshal example XML: %v", err)
+ }
+
+ // OS loader should be accessible
+ loader := capabilities.OS.Loader
+ _ = loader.Supported
+
+ // Enums should be accessible
+ if len(loader.Enums) > 0 {
+ for _, enum := range loader.Enums {
+ if enum.Name == "" {
+ t.Error("Expected enum to have a name")
+ }
+ // Values should be accessible
+ _ = enum.Values
+ }
+ }
+}
+
+func TestClientEmulator_Get_NoPanic(t *testing.T) {
+ client := NewClientEmulator()
+
+ // Ensure calling Get doesn't panic even with nil libvirt
+ defer func() {
+ if r := recover(); r != nil {
+ t.Errorf("Get() panicked with nil libvirt: %v", r)
+ }
+ }()
+
+ _, err := client.Get(nil)
+ if err != nil {
+ t.Fatalf("Get() returned unexpected error: %v", err)
+ }
+}
+
+func TestExampleXML_DomainType(t *testing.T) {
+ var capabilities DomainCapabilities
+ if err := xml.Unmarshal(exampleXML, &capabilities); err != nil {
+ t.Fatalf("Failed to unmarshal example XML: %v", err)
+ }
+
+ // Domain type should typically be "kvm", "qemu", etc.
+ if capabilities.Domain == "" {
+ t.Error("Expected domain type to be set")
+ }
+
+ // Common domain types
+ validTypes := map[string]bool{
+ "kvm": true,
+ "qemu": true,
+ "xen": true,
+ "lxc": true,
+ }
+
+ if !validTypes[capabilities.Domain] {
+ t.Logf("Note: Unusual domain type '%s' (expected kvm, qemu, xen, or lxc)", capabilities.Domain)
+ }
+}
+
+func TestExampleXML_Architecture(t *testing.T) {
+ var capabilities DomainCapabilities
+ if err := xml.Unmarshal(exampleXML, &capabilities); err != nil {
+ t.Fatalf("Failed to unmarshal example XML: %v", err)
+ }
+
+ // Architecture should be set
+ if capabilities.Arch == "" {
+ t.Error("Expected architecture to be set")
+ }
+
+ // Common architectures
+ validArchs := map[string]bool{
+ "x86_64": true,
+ "i686": true,
+ "aarch64": true,
+ "ppc64": true,
+ "ppc64le": true,
+ "s390x": true,
+ }
+
+ if !validArchs[capabilities.Arch] {
+ t.Logf("Note: Unusual architecture '%s'", capabilities.Arch)
+ }
+}
+
+func TestExampleXML_DeviceEnums(t *testing.T) {
+ var capabilities DomainCapabilities
+ if err := xml.Unmarshal(exampleXML, &capabilities); err != nil {
+ t.Fatalf("Failed to unmarshal example XML: %v", err)
+ }
+
+ if len(capabilities.Devices.Devices) == 0 {
+ t.Skip("No devices in example XML")
+ }
+
+ // Verify device enum structure
+ for _, device := range capabilities.Devices.Devices {
+ for _, enum := range device.Enums {
+ if enum.Name == "" {
+ t.Error("Expected device enum to have a name")
+ }
+ if len(enum.Values) == 0 {
+ t.Logf("Note: Device enum '%s' has no values", enum.Name)
+ }
+ }
+ }
+}
+
+func TestExampleXML_FeaturesList(t *testing.T) {
+ var capabilities DomainCapabilities
+ if err := xml.Unmarshal(exampleXML, &capabilities); err != nil {
+ t.Fatalf("Failed to unmarshal example XML: %v", err)
+ }
+
+ if len(capabilities.Features.Features) == 0 {
+ t.Skip("No features in example XML")
+ }
+
+ // Verify we can iterate through features
+ for _, feature := range capabilities.Features.Features {
+ // Supported attribute should be accessible
+ _ = feature.Supported
+ }
+}
+
+func TestClientEmulator_Get_MultipleFields(t *testing.T) {
+ client := NewClientEmulator()
+ result, err := client.Get(nil)
+
+ if err != nil {
+ t.Fatalf("Get() returned unexpected error: %v", err)
+ }
+
+ // Count how many main fields are populated
+ fieldsSet := 0
+ if result.Domain != "" {
+ fieldsSet++
+ }
+ if result.Arch != "" {
+ fieldsSet++
+ }
+ if len(result.CPU.Modes) > 0 {
+ fieldsSet++
+ }
+ if len(result.Devices.Devices) > 0 {
+ fieldsSet++
+ }
+ if len(result.Features.Features) > 0 {
+ fieldsSet++
+ }
+
+ if fieldsSet == 0 {
+ t.Error("Expected at least one field to be populated in domain capabilities")
+ }
+}
diff --git a/internal/libvirt/domcapabilities/example.go b/internal/libvirt/domcapabilities/example.go
new file mode 100644
index 0000000..328a7db
--- /dev/null
+++ b/internal/libvirt/domcapabilities/example.go
@@ -0,0 +1,25 @@
+/*
+SPDX-FileCopyrightText: Copyright 2025 SAP SE or an SAP affiliate company and cobaltcore-dev contributors
+SPDX-License-Identifier: Apache-2.0
+
+Licensed under the Apache License, LibVirtVersion 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package domcapabilities
+
+import (
+ _ "embed"
+)
+
+//go:embed example.xml
+var exampleXML []byte
diff --git a/internal/libvirt/domcapabilities/example.xml b/internal/libvirt/domcapabilities/example.xml
new file mode 100644
index 0000000..723bf89
--- /dev/null
+++ b/internal/libvirt/domcapabilities/example.xml
@@ -0,0 +1,33 @@
+
+
+
+
+ ch
+ x86_64
+
+
+
+ no
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/internal/libvirt/domcapabilities/schema.go b/internal/libvirt/domcapabilities/schema.go
new file mode 100644
index 0000000..16fe572
--- /dev/null
+++ b/internal/libvirt/domcapabilities/schema.go
@@ -0,0 +1,87 @@
+/*
+SPDX-FileCopyrightText: Copyright 2025 SAP SE or an SAP affiliate company and cobaltcore-dev contributors
+SPDX-License-Identifier: Apache-2.0
+
+Licensed under the Apache License, LibVirtVersion 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package domcapabilities
+
+import "encoding/xml"
+
+// DomainCapabilities as returned from the libvirt domain capabilities api.
+//
+// The format is the same as returned when executing `virsh domcapabilities`.
+// See: https://www.libvirt.org/manpages/virsh.html#domcapabilities
+// For another reference see: https://gitlab.com/libvirt/libvirt-go-xml-module/-/blob/v1.11010.0/domain_capabilities.go
+type DomainCapabilities struct {
+ Domain string `xml:"domain"`
+ Arch string `xml:"arch"`
+ OS DomainCapabilitiesOS `xml:"os"`
+ CPU DomainCapabilitiesCPU `xml:"cpu"`
+ Devices DomainCapabilitiesDevices `xml:"devices"`
+ Features DomainCapabilitiesFeatures `xml:"features"`
+}
+
+// DomainCapabilitiesOS represents the OS capabilities section.
+type DomainCapabilitiesOS struct {
+ Supported string `xml:"supported,attr"`
+ Loader DomainCapabilitiesOSLoader `xml:"loader"`
+}
+
+// DomainCapabilitiesOSLoader represents the loader capabilities.
+type DomainCapabilitiesOSLoader struct {
+ Supported string `xml:"supported,attr"`
+ Enums []DomainCapabilitiesEnum `xml:"enum"`
+}
+
+// DomainCapabilitiesEnum represents an enumeration of possible values.
+type DomainCapabilitiesEnum struct {
+ Name string `xml:"name,attr"`
+ Values []string `xml:"value"`
+}
+
+// DomainCapabilitiesCPU represents the CPU capabilities section.
+type DomainCapabilitiesCPU struct {
+ Modes []DomainCapabilitiesCPUMode `xml:"mode"`
+}
+
+// DomainCapabilitiesCPUMode represents a CPU mode with its capabilities.
+type DomainCapabilitiesCPUMode struct {
+ Name string `xml:"name,attr"`
+ Supported string `xml:"supported,attr"`
+ Enums []DomainCapabilitiesEnum `xml:"enum"`
+}
+
+// DomainCapabilitiesDevice represents the devices capabilities section.
+type DomainCapabilitiesDevice struct {
+ XMLName xml.Name `xml:""`
+ Supported string `xml:"supported,attr"`
+ Enums []DomainCapabilitiesEnum `xml:"enum"`
+}
+
+// DomainCapabilitiesDevices represents the devices capabilities section.
+type DomainCapabilitiesDevices struct {
+ Devices []DomainCapabilitiesDevice `xml:",any"`
+}
+
+// DomainCapabilitiesFeature represents a feature with supported attribute.
+type DomainCapabilitiesFeature struct {
+ XMLName xml.Name `xml:""`
+ Supported string `xml:"supported,attr"`
+}
+
+// DomainCapabilitiesFeatures represents the features capabilities section.
+type DomainCapabilitiesFeatures struct {
+ Features []DomainCapabilitiesFeature `xml:",any"`
+}
diff --git a/internal/libvirt/domcapabilities/schema_test.go b/internal/libvirt/domcapabilities/schema_test.go
new file mode 100644
index 0000000..0dc370b
--- /dev/null
+++ b/internal/libvirt/domcapabilities/schema_test.go
@@ -0,0 +1,206 @@
+/*
+SPDX-FileCopyrightText: Copyright 2025 SAP SE or an SAP affiliate company and cobaltcore-dev contributors
+SPDX-License-Identifier: Apache-2.0
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package domcapabilities
+
+import (
+ "encoding/xml"
+ "testing"
+)
+
+func TestDomainCapabilitiesDeserialization(t *testing.T) {
+ // Unmarshal the XML into our DomainCapabilities struct
+ var domainCapabilities DomainCapabilities
+ err := xml.Unmarshal(exampleXML, &domainCapabilities)
+ if err != nil {
+ t.Fatalf("Failed to unmarshal XML: %v", err)
+ }
+
+ // Verify domain and arch
+ if domainCapabilities.Domain != "ch" {
+ t.Errorf("Expected domain to be 'ch', got '%s'", domainCapabilities.Domain)
+ }
+ if domainCapabilities.Arch != "x86_64" {
+ t.Errorf("Expected arch to be 'x86_64', got '%s'", domainCapabilities.Arch)
+ }
+
+ // Verify OS section
+ if domainCapabilities.OS.Supported != "yes" {
+ t.Errorf("Expected OS supported to be 'yes', got '%s'", domainCapabilities.OS.Supported)
+ }
+ if domainCapabilities.OS.Loader.Supported != "yes" {
+ t.Errorf("Expected OS loader supported to be 'yes', got '%s'", domainCapabilities.OS.Loader.Supported)
+ }
+ if len(domainCapabilities.OS.Loader.Enums) != 1 {
+ t.Errorf("Expected 1 loader enum, got %d", len(domainCapabilities.OS.Loader.Enums))
+ }
+ if domainCapabilities.OS.Loader.Enums[0].Name != "secure" {
+ t.Errorf("Expected loader enum name to be 'secure', got '%s'", domainCapabilities.OS.Loader.Enums[0].Name)
+ }
+ if len(domainCapabilities.OS.Loader.Enums[0].Values) != 1 {
+ t.Errorf("Expected 1 value in secure enum, got %d", len(domainCapabilities.OS.Loader.Enums[0].Values))
+ }
+ if domainCapabilities.OS.Loader.Enums[0].Values[0] != "no" {
+ t.Errorf("Expected secure enum value to be 'no', got '%s'", domainCapabilities.OS.Loader.Enums[0].Values[0])
+ }
+
+ // Verify CPU section
+ if len(domainCapabilities.CPU.Modes) != 4 {
+ t.Errorf("Expected 4 CPU modes, got %d", len(domainCapabilities.CPU.Modes))
+ }
+
+ // Verify host-passthrough mode
+ hostPassthroughMode := domainCapabilities.CPU.Modes[0]
+ if hostPassthroughMode.Name != "host-passthrough" {
+ t.Errorf("Expected first CPU mode name to be 'host-passthrough', got '%s'", hostPassthroughMode.Name)
+ }
+ if hostPassthroughMode.Supported != "yes" {
+ t.Errorf("Expected host-passthrough mode supported to be 'yes', got '%s'", hostPassthroughMode.Supported)
+ }
+ if len(hostPassthroughMode.Enums) != 1 {
+ t.Errorf("Expected 1 enum for host-passthrough mode, got %d", len(hostPassthroughMode.Enums))
+ }
+ if hostPassthroughMode.Enums[0].Name != "hostPassthroughMigratable" {
+ t.Errorf("Expected enum name to be 'hostPassthroughMigratable', got '%s'", hostPassthroughMode.Enums[0].Name)
+ }
+
+ // Verify maximum mode
+ maximumMode := domainCapabilities.CPU.Modes[1]
+ if maximumMode.Name != "maximum" {
+ t.Errorf("Expected second CPU mode name to be 'maximum', got '%s'", maximumMode.Name)
+ }
+ if maximumMode.Supported != "no" {
+ t.Errorf("Expected maximum mode supported to be 'no', got '%s'", maximumMode.Supported)
+ }
+
+ // Verify host-model mode
+ hostModelMode := domainCapabilities.CPU.Modes[2]
+ if hostModelMode.Name != "host-model" {
+ t.Errorf("Expected third CPU mode name to be 'host-model', got '%s'", hostModelMode.Name)
+ }
+ if hostModelMode.Supported != "no" {
+ t.Errorf("Expected host-model mode supported to be 'no', got '%s'", hostModelMode.Supported)
+ }
+
+ // Verify custom mode
+ customMode := domainCapabilities.CPU.Modes[3]
+ if customMode.Name != "custom" {
+ t.Errorf("Expected fourth CPU mode name to be 'custom', got '%s'", customMode.Name)
+ }
+ if customMode.Supported != "no" {
+ t.Errorf("Expected custom mode supported to be 'no', got '%s'", customMode.Supported)
+ }
+
+ // Verify devices section
+ if len(domainCapabilities.Devices.Devices) != 1 {
+ t.Errorf("Expected 1 device, got %d", len(domainCapabilities.Devices.Devices))
+ }
+ videoDevice := domainCapabilities.Devices.Devices[0]
+ if videoDevice.XMLName.Local != "video" {
+ t.Errorf("Expected device name to be 'video', got '%s'", videoDevice.XMLName.Local)
+ }
+ if videoDevice.Supported != "yes" {
+ t.Errorf("Expected video device supported to be 'yes', got '%s'", videoDevice.Supported)
+ }
+ if len(videoDevice.Enums) != 1 {
+ t.Errorf("Expected 1 enum for video device, got %d", len(videoDevice.Enums))
+ }
+ if videoDevice.Enums[0].Name != "modelType" {
+ t.Errorf("Expected video enum name to be 'modelType', got '%s'", videoDevice.Enums[0].Name)
+ }
+ if len(videoDevice.Enums[0].Values) != 1 {
+ t.Errorf("Expected 1 value in modelType enum, got %d", len(videoDevice.Enums[0].Values))
+ }
+ if videoDevice.Enums[0].Values[0] != "none" {
+ t.Errorf("Expected modelType value to be 'none', got '%s'", videoDevice.Enums[0].Values[0])
+ }
+
+ // Verify features section
+ if len(domainCapabilities.Features.Features) != 2 {
+ t.Errorf("Expected 2 features, got %d", len(domainCapabilities.Features.Features))
+ }
+
+ // Verify sev feature
+ sevFeature := domainCapabilities.Features.Features[0]
+ if sevFeature.XMLName.Local != "sev" {
+ t.Errorf("Expected first feature name to be 'sev', got '%s'", sevFeature.XMLName.Local)
+ }
+ if sevFeature.Supported != "no" {
+ t.Errorf("Expected sev feature supported to be 'no', got '%s'", sevFeature.Supported)
+ }
+
+ // Verify sgx feature
+ sgxFeature := domainCapabilities.Features.Features[1]
+ if sgxFeature.XMLName.Local != "sgx" {
+ t.Errorf("Expected second feature name to be 'sgx', got '%s'", sgxFeature.XMLName.Local)
+ }
+ if sgxFeature.Supported != "no" {
+ t.Errorf("Expected sgx feature supported to be 'no', got '%s'", sgxFeature.Supported)
+ }
+}
+
+func TestDomainCapabilitiesRoundTrip(t *testing.T) {
+ // Unmarshal into struct
+ var domainCapabilities DomainCapabilities
+ err := xml.Unmarshal(exampleXML, &domainCapabilities)
+ if err != nil {
+ t.Fatalf("Failed to unmarshal XML: %v", err)
+ }
+
+ // Marshal back to XML
+ marshaledXML, err := xml.MarshalIndent(domainCapabilities, "", " ")
+ if err != nil {
+ t.Fatalf("Failed to marshal back to XML: %v", err)
+ }
+
+ // Unmarshal the marshaled XML to verify it's still valid
+ var roundTripDomainCapabilities DomainCapabilities
+ err = xml.Unmarshal(marshaledXML, &roundTripDomainCapabilities)
+ if err != nil {
+ t.Fatalf("Failed to unmarshal round-trip XML: %v", err)
+ }
+
+ // Verify key fields are preserved
+ if domainCapabilities.Domain != roundTripDomainCapabilities.Domain {
+ t.Errorf("Domain mismatch after round trip: expected '%s', got '%s'",
+ domainCapabilities.Domain, roundTripDomainCapabilities.Domain)
+ }
+ if domainCapabilities.Arch != roundTripDomainCapabilities.Arch {
+ t.Errorf("Arch mismatch after round trip: expected '%s', got '%s'",
+ domainCapabilities.Arch, roundTripDomainCapabilities.Arch)
+ }
+ if domainCapabilities.OS.Supported != roundTripDomainCapabilities.OS.Supported {
+ t.Errorf("OS supported mismatch after round trip: expected '%s', got '%s'",
+ domainCapabilities.OS.Supported, roundTripDomainCapabilities.OS.Supported)
+ }
+ if domainCapabilities.OS.Loader.Supported != roundTripDomainCapabilities.OS.Loader.Supported {
+ t.Errorf("OS loader supported mismatch after round trip: expected '%s', got '%s'",
+ domainCapabilities.OS.Loader.Supported, roundTripDomainCapabilities.OS.Loader.Supported)
+ }
+ if len(domainCapabilities.CPU.Modes) != len(roundTripDomainCapabilities.CPU.Modes) {
+ t.Errorf("CPU modes count mismatch after round trip: expected %d, got %d",
+ len(domainCapabilities.CPU.Modes), len(roundTripDomainCapabilities.CPU.Modes))
+ }
+ if len(domainCapabilities.Devices.Devices) != len(roundTripDomainCapabilities.Devices.Devices) {
+ t.Errorf("Devices count mismatch after round trip: expected %d, got %d",
+ len(domainCapabilities.Devices.Devices), len(roundTripDomainCapabilities.Devices.Devices))
+ }
+ if len(domainCapabilities.Features.Features) != len(roundTripDomainCapabilities.Features.Features) {
+ t.Errorf("Features count mismatch after round trip: expected %d, got %d",
+ len(domainCapabilities.Features.Features), len(roundTripDomainCapabilities.Features.Features))
+ }
+}
diff --git a/internal/libvirt/dominfo/client.go b/internal/libvirt/dominfo/client.go
new file mode 100644
index 0000000..af88c23
--- /dev/null
+++ b/internal/libvirt/dominfo/client.go
@@ -0,0 +1,82 @@
+/*
+SPDX-FileCopyrightText: Copyright 2025 SAP SE or an SAP affiliate company and cobaltcore-dev contributors
+SPDX-License-Identifier: Apache-2.0
+
+Licensed under the Apache License, LibVirtVersion 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package dominfo
+
+import (
+ "encoding/xml"
+
+ libvirt "github.com/digitalocean/go-libvirt"
+ "sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+// Client that returns information for all domains on our host.
+type Client interface {
+ // Return information for all domains on our host.
+ Get(virt *libvirt.Libvirt) ([]DomainInfo, error)
+}
+
+// Implementation of the Client interface.
+type client struct{}
+
+// Create a new domain info client.
+func NewClient() Client {
+ return &client{}
+}
+
+// Return information for all domains on our host.
+func (m *client) Get(virt *libvirt.Libvirt) ([]DomainInfo, error) {
+ domains, _, err := virt.ConnectListAllDomains(1,
+ libvirt.ConnectListDomainsActive|libvirt.ConnectListDomainsInactive)
+ if err != nil {
+ log.Log.Error(err, "failed to list all domains")
+ return nil, err
+ }
+ var domainInfos []DomainInfo
+ for _, domain := range domains {
+ domainXML, err := virt.DomainGetXMLDesc(domain, 0)
+ if err != nil {
+ log.Log.Error(err, "failed to get domain xml", "domain", domain.Name)
+ return nil, err
+ }
+ var domainInfo DomainInfo
+ if err := xml.Unmarshal([]byte(domainXML), &domainInfo); err != nil {
+ log.Log.Error(err, "failed to unmarshal domain xml", "domain", domain.Name)
+ return nil, err
+ }
+ domainInfos = append(domainInfos, domainInfo)
+ }
+ return domainInfos, nil
+}
+
+// Emulated domain info client returning an embedded domain xml.
+type clientEmulator struct{}
+
+// Create a new emulated domain info client.
+func NewClientEmulator() Client {
+ return &clientEmulator{}
+}
+
+// Get the domain infos of the host we are mounted on.
+func (c *clientEmulator) Get(virt *libvirt.Libvirt) ([]DomainInfo, error) {
+ var info DomainInfo
+ if err := xml.Unmarshal(exampleXML, &info); err != nil {
+ log.Log.Error(err, "failed to unmarshal example capabilities")
+ return nil, err
+ }
+ return []DomainInfo{info}, nil
+}
diff --git a/internal/libvirt/dominfo/client_test.go b/internal/libvirt/dominfo/client_test.go
new file mode 100644
index 0000000..58e810d
--- /dev/null
+++ b/internal/libvirt/dominfo/client_test.go
@@ -0,0 +1,349 @@
+/*
+SPDX-FileCopyrightText: Copyright 2025 SAP SE or an SAP affiliate company and cobaltcore-dev contributors
+SPDX-License-Identifier: Apache-2.0
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package dominfo
+
+import (
+ "encoding/xml"
+ "testing"
+)
+
+func TestNewClient(t *testing.T) {
+ client := NewClient()
+ if client == nil {
+ t.Fatal("NewClient() returned nil")
+ }
+
+ // Verify it implements the Client interface
+ var _ = client
+}
+
+func TestNewClient_ReturnsCorrectType(t *testing.T) {
+ client := NewClient()
+ // Verify it returns a non-nil implementation
+ if client == nil {
+ t.Fatal("NewClient() returned nil")
+ }
+ // Verify it's not the emulator type by checking behavior
+ // (we can't check the unexported type directly)
+}
+
+func TestNewClientEmulator(t *testing.T) {
+ client := NewClientEmulator()
+ if client == nil {
+ t.Fatal("NewClientEmulator() returned nil")
+ }
+
+ // Verify it implements the Client interface
+ var _ = client
+}
+
+func TestNewClientEmulator_ReturnsCorrectType(t *testing.T) {
+ client := NewClientEmulator()
+ // Verify it returns a non-nil implementation
+ if client == nil {
+ t.Fatal("NewClientEmulator() returned nil")
+ }
+ // Verify it works without a libvirt connection (emulator behavior)
+ _, err := client.Get(nil)
+ if err != nil {
+ t.Errorf("Emulator should work with nil libvirt connection, got error: %v", err)
+ }
+}
+
+func TestClientEmulator_Get_Success(t *testing.T) {
+ client := NewClientEmulator()
+
+ // The emulator doesn't actually use the libvirt connection,
+ // so we pass nil to test it doesn't panic
+ domainInfos, err := client.Get(nil)
+
+ if err != nil {
+ t.Fatalf("Get() returned unexpected error: %v", err)
+ }
+
+ if len(domainInfos) != 1 {
+ t.Fatalf("Expected 1 domain info from emulator, got %d", len(domainInfos))
+ }
+
+ // Verify the returned domain info has expected structure
+ if domainInfos[0].Name == "" {
+ t.Error("Expected domain to have a name")
+ }
+
+ if domainInfos[0].UUID == "" {
+ t.Error("Expected domain to have a UUID")
+ }
+}
+
+func TestClientEmulator_Get_ValidXML(t *testing.T) {
+ client := NewClientEmulator()
+ domainInfos, err := client.Get(nil)
+
+ if err != nil {
+ t.Fatalf("Get() returned unexpected error: %v", err)
+ }
+
+ // Verify the embedded XML can be parsed correctly
+ var testInfo DomainInfo
+ if err := xml.Unmarshal(exampleXML, &testInfo); err != nil {
+ t.Fatalf("Failed to unmarshal example XML: %v", err)
+ }
+
+ // The emulator should return the same data
+ if domainInfos[0].Name != testInfo.Name {
+ t.Errorf("Expected domain name '%s', got '%s'", testInfo.Name, domainInfos[0].Name)
+ }
+
+ if domainInfos[0].UUID != testInfo.UUID {
+ t.Errorf("Expected domain UUID '%s', got '%s'", testInfo.UUID, domainInfos[0].UUID)
+ }
+
+ if domainInfos[0].Type != testInfo.Type {
+ t.Errorf("Expected domain type '%s', got '%s'", testInfo.Type, domainInfos[0].Type)
+ }
+}
+
+func TestClientEmulator_Get_Consistency(t *testing.T) {
+ client := NewClientEmulator()
+
+ // Call Get multiple times and verify consistent results
+ results1, err1 := client.Get(nil)
+ if err1 != nil {
+ t.Fatalf("First Get() call failed: %v", err1)
+ }
+
+ results2, err2 := client.Get(nil)
+ if err2 != nil {
+ t.Fatalf("Second Get() call failed: %v", err2)
+ }
+
+ if len(results1) != len(results2) {
+ t.Errorf("Inconsistent results: first call returned %d domains, second returned %d",
+ len(results1), len(results2))
+ }
+
+ if len(results1) > 0 && len(results2) > 0 {
+ if results1[0].Name != results2[0].Name {
+ t.Errorf("Inconsistent domain names: '%s' vs '%s'",
+ results1[0].Name, results2[0].Name)
+ }
+ if results1[0].UUID != results2[0].UUID {
+ t.Errorf("Inconsistent domain UUIDs: '%s' vs '%s'",
+ results1[0].UUID, results2[0].UUID)
+ }
+ }
+}
+
+func TestClientEmulator_Get_MemoryInfo(t *testing.T) {
+ client := NewClientEmulator()
+ domainInfos, err := client.Get(nil)
+
+ if err != nil {
+ t.Fatalf("Get() returned unexpected error: %v", err)
+ }
+
+ if len(domainInfos) == 0 {
+ t.Fatal("Expected at least one domain info")
+ }
+
+ domain := domainInfos[0]
+
+ // Check that memory information is present
+ if domain.Memory != nil {
+ if domain.Memory.Value <= 0 {
+ t.Error("Expected memory value to be positive")
+ }
+ if domain.Memory.Unit == "" {
+ t.Error("Expected memory unit to be set")
+ }
+ }
+
+ if domain.CurrentMemory != nil {
+ if domain.CurrentMemory.Value <= 0 {
+ t.Error("Expected current memory value to be positive")
+ }
+ }
+}
+
+func TestClientEmulator_Get_VCPUInfo(t *testing.T) {
+ client := NewClientEmulator()
+ domainInfos, err := client.Get(nil)
+
+ if err != nil {
+ t.Fatalf("Get() returned unexpected error: %v", err)
+ }
+
+ if len(domainInfos) == 0 {
+ t.Fatal("Expected at least one domain info")
+ }
+
+ domain := domainInfos[0]
+
+ // Check that VCPU information is present
+ if domain.VCPU != nil {
+ if domain.VCPU.Value <= 0 {
+ t.Error("Expected VCPU count to be positive")
+ }
+ }
+}
+
+func TestClientEmulator_Get_MetadataInfo(t *testing.T) {
+ client := NewClientEmulator()
+ domainInfos, err := client.Get(nil)
+
+ if err != nil {
+ t.Fatalf("Get() returned unexpected error: %v", err)
+ }
+
+ if len(domainInfos) == 0 {
+ t.Fatal("Expected at least one domain info")
+ }
+
+ domain := domainInfos[0]
+
+ // Check that metadata can be accessed (if present)
+ // This is optional, so we just verify it doesn't cause issues
+ if domain.Metadata != nil && domain.Metadata.NovaInstance != nil {
+ // Just verify we can access these fields without panic
+ _ = domain.Metadata.NovaInstance.Name
+ _ = domain.Metadata.NovaInstance.CreationTime
+ }
+}
+
+func TestClientEmulator_Get_DevicesInfo(t *testing.T) {
+ client := NewClientEmulator()
+ domainInfos, err := client.Get(nil)
+
+ if err != nil {
+ t.Fatalf("Get() returned unexpected error: %v", err)
+ }
+
+ if len(domainInfos) == 0 {
+ t.Fatal("Expected at least one domain info")
+ }
+
+ domain := domainInfos[0]
+
+ // Check that devices information can be accessed (if present)
+ if domain.Devices != nil {
+ // Just verify we can access these fields without panic
+ _ = domain.Devices.Emulator
+ _ = domain.Devices.Disks
+ _ = domain.Devices.Interfaces
+ }
+}
+
+func TestClient_InterfaceCompliance(t *testing.T) {
+ // Ensure both implementations satisfy the Client interface
+ var _ = NewClient()
+ var _ = NewClientEmulator()
+}
+
+func TestExampleXML_IsValid(t *testing.T) {
+ // Verify that the embedded example XML can be parsed
+ var info DomainInfo
+ if err := xml.Unmarshal(exampleXML, &info); err != nil {
+ t.Fatalf("Failed to unmarshal example XML: %v", err)
+ }
+
+ // Basic validation that we got some data
+ if info.Name == "" {
+ t.Error("Expected domain name to be present in example XML")
+ }
+ if info.UUID == "" {
+ t.Error("Expected domain UUID to be present in example XML")
+ }
+ if info.Type == "" {
+ t.Error("Expected domain type to be present in example XML")
+ }
+}
+
+func TestExampleXML_Structure(t *testing.T) {
+ var info DomainInfo
+ if err := xml.Unmarshal(exampleXML, &info); err != nil {
+ t.Fatalf("Failed to unmarshal example XML: %v", err)
+ }
+
+ // Test that the example XML has reasonable structure
+ tests := []struct {
+ name string
+ checkFunc func() bool
+ errMsg string
+ }{
+ {
+ name: "Has Memory Info",
+ checkFunc: func() bool { return info.Memory != nil },
+ errMsg: "Expected Memory field to be populated",
+ },
+ {
+ name: "Has VCPU Info",
+ checkFunc: func() bool { return info.VCPU != nil },
+ errMsg: "Expected VCPU field to be populated",
+ },
+ {
+ name: "Has OS Info",
+ checkFunc: func() bool { return info.OS != nil },
+ errMsg: "Expected OS field to be populated",
+ },
+ {
+ name: "Has Devices Info",
+ checkFunc: func() bool { return info.Devices != nil },
+ errMsg: "Expected Devices field to be populated",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if !tt.checkFunc() {
+ t.Error(tt.errMsg)
+ }
+ })
+ }
+}
+
+func TestClientEmulator_Get_ReturnsSlice(t *testing.T) {
+ client := NewClientEmulator()
+ result, err := client.Get(nil)
+
+ if err != nil {
+ t.Fatalf("Get() returned unexpected error: %v", err)
+ }
+
+ // Verify the result is a slice
+ if result == nil {
+ t.Fatal("Expected non-nil slice, got nil")
+ }
+
+ // Verify the slice has elements
+ if len(result) == 0 {
+ t.Error("Expected at least one element in the result slice")
+ }
+}
+
+func TestClientTypes_AreDistinct(t *testing.T) {
+ client1 := NewClient()
+ client2 := NewClientEmulator()
+
+ // Verify they are different types
+ type1 := any(client1)
+ type2 := any(client2)
+
+ if type1 == type2 {
+ t.Error("Expected NewClient() and NewClientEmulator() to return different types")
+ }
+}
diff --git a/internal/libvirt/dominfo/example.go b/internal/libvirt/dominfo/example.go
new file mode 100644
index 0000000..a0fc413
--- /dev/null
+++ b/internal/libvirt/dominfo/example.go
@@ -0,0 +1,25 @@
+/*
+SPDX-FileCopyrightText: Copyright 2025 SAP SE or an SAP affiliate company and cobaltcore-dev contributors
+SPDX-License-Identifier: Apache-2.0
+
+Licensed under the Apache License, LibVirtVersion 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package dominfo
+
+import (
+ _ "embed"
+)
+
+//go:embed example.xml
+var exampleXML []byte
diff --git a/internal/libvirt/dominfo/example.xml b/internal/libvirt/dominfo/example.xml
new file mode 100644
index 0000000..61b4292
--- /dev/null
+++ b/internal/libvirt/dominfo/example.xml
@@ -0,0 +1,95 @@
+
+
+
+
+ instance-12345-abc
+ 12345-abc
+
+
+
+ example-12345-abc
+ 2025-12-18 00:49:23
+
+ 24560
+ 0
+ 0
+ 0
+ 6
+
+
+ example-user
+ example-project
+
+
+
+
+
+
+
+
+
+ 25149440
+ 25149440
+
+
+
+
+
+ 6
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /machine
+
+
+ hvm
+ /usr/share/cloud-hypervisor/CLOUDHV_EFI.fd
+
+
+
+
+
+ |
+
+
+
+ destroy
+ restart
+ destroy
+
+ /usr/bin/cloud-hypervisor
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/internal/libvirt/dominfo/schema.go b/internal/libvirt/dominfo/schema.go
new file mode 100644
index 0000000..d06e0f7
--- /dev/null
+++ b/internal/libvirt/dominfo/schema.go
@@ -0,0 +1,370 @@
+/*
+SPDX-FileCopyrightText: Copyright 2025 SAP SE or an SAP affiliate company and cobaltcore-dev contributors
+SPDX-License-Identifier: Apache-2.0
+
+Licensed under the Apache License, LibVirtVersion 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package dominfo
+
+import "encoding/xml"
+
+// DomainInfo as returned from the libvirt dumpxml api.
+//
+// The format is the same as returned when executing `virsh dumpxml`.
+// See: https://www.libvirt.org/manpages/virsh.html#dumpxml
+// For another reference see: https://gitlab.com/libvirt/libvirt-go-xml-module/-/blob/v1.11010.0/domain.go#L3237
+type DomainInfo struct {
+ Type string `xml:"type,attr"`
+ ID string `xml:"id,attr,omitempty"`
+ Name string `xml:"name"`
+ UUID string `xml:"uuid"`
+ Metadata *DomainMetadata `xml:"metadata,omitempty"`
+ Memory *DomainMemory `xml:"memory,omitempty"`
+ CurrentMemory *DomainMemory `xml:"currentMemory,omitempty"`
+ MemoryBacking *DomainMemoryBacking `xml:"memoryBacking,omitempty"`
+ VCPU *DomainVCPU `xml:"vcpu,omitempty"`
+ CPUTune *DomainCPUTune `xml:"cputune,omitempty"`
+ NumaTune *DomainNumaTune `xml:"numatune,omitempty"`
+ Resource *DomainResource `xml:"resource,omitempty"`
+ OS *DomainOS `xml:"os,omitempty"`
+ CPU *DomainCPU `xml:"cpu,omitempty"`
+ Clock *DomainClock `xml:"clock,omitempty"`
+ OnPoweroff string `xml:"on_poweroff,omitempty"`
+ OnReboot string `xml:"on_reboot,omitempty"`
+ OnCrash string `xml:"on_crash,omitempty"`
+ Devices *DomainDevices `xml:"devices,omitempty"`
+}
+
+// DomainMetadata represents the metadata section containing OpenStack Nova information.
+type DomainMetadata struct {
+ NovaInstance *NovaInstance `xml:"instance"`
+}
+
+// NovaInstance represents OpenStack Nova instance metadata.
+type NovaInstance struct {
+ XMLName xml.Name `xml:"http://openstack.org/xmlns/libvirt/nova/1.1 instance"`
+ Package *NovaPackage `xml:"package,omitempty"`
+ Name string `xml:"name,omitempty"`
+ CreationTime string `xml:"creationTime,omitempty"`
+ Flavor *NovaFlavor `xml:"flavor,omitempty"`
+ Owner *NovaOwner `xml:"owner,omitempty"`
+ Root *NovaRoot `xml:"root,omitempty"`
+ Ports *NovaPorts `xml:"ports,omitempty"`
+}
+
+// NovaPackage represents the Nova package version.
+type NovaPackage struct {
+ Version string `xml:"version,attr"`
+}
+
+// NovaFlavor represents the instance flavor.
+type NovaFlavor struct {
+ Name string `xml:"name,attr"`
+ Memory int `xml:"memory"`
+ Disk int `xml:"disk"`
+ Swap int `xml:"swap"`
+ Ephemeral int `xml:"ephemeral"`
+ VCPUs int `xml:"vcpus"`
+}
+
+// NovaOwner represents the instance owner.
+type NovaOwner struct {
+ User *NovaUser `xml:"user,omitempty"`
+ Project *NovaProject `xml:"project,omitempty"`
+}
+
+// NovaUser represents the user who owns the instance.
+type NovaUser struct {
+ UUID string `xml:"uuid,attr"`
+ Value string `xml:",chardata"`
+}
+
+// NovaProject represents the project that owns the instance.
+type NovaProject struct {
+ UUID string `xml:"uuid,attr"`
+ Value string `xml:",chardata"`
+}
+
+// NovaRoot represents the root image.
+type NovaRoot struct {
+ Type string `xml:"type,attr"`
+ UUID string `xml:"uuid,attr"`
+}
+
+// NovaPorts represents the network ports.
+type NovaPorts struct {
+ Ports []NovaPort `xml:"port"`
+}
+
+// NovaPort represents a network port.
+type NovaPort struct {
+ UUID string `xml:"uuid,attr"`
+ IPs []NovaIP `xml:"ip"`
+}
+
+// NovaIP represents an IP address.
+type NovaIP struct {
+ Type string `xml:"type,attr"`
+ Address string `xml:"address,attr"`
+ IPVersion string `xml:"ipVersion,attr"`
+}
+
+// DomainMemory represents memory configuration.
+type DomainMemory struct {
+ Unit string `xml:"unit,attr"`
+ Value int64 `xml:",chardata"`
+}
+
+// DomainMemoryBacking represents memory backing configuration.
+type DomainMemoryBacking struct {
+ HugePages *DomainHugePages `xml:"hugepages,omitempty"`
+}
+
+// DomainHugePages represents huge pages configuration.
+type DomainHugePages struct {
+ Pages []DomainPage `xml:"page"`
+}
+
+// DomainPage represents a huge page configuration.
+type DomainPage struct {
+ Size string `xml:"size,attr"`
+ Unit string `xml:"unit,attr"`
+ Nodeset string `xml:"nodeset,attr,omitempty"`
+}
+
+// DomainVCPU represents virtual CPU configuration.
+type DomainVCPU struct {
+ Placement string `xml:"placement,attr,omitempty"`
+ Value int `xml:",chardata"`
+}
+
+// DomainCPUTune represents CPU tuning configuration.
+type DomainCPUTune struct {
+ VCPUPins []DomainVCPUPin `xml:"vcpupin,omitempty"`
+ EmulatorPin *DomainCPUPin `xml:"emulatorpin,omitempty"`
+}
+
+// DomainVCPUPin represents a VCPU pinning configuration.
+type DomainVCPUPin struct {
+ VCPU int `xml:"vcpu,attr"`
+ CPUSet string `xml:"cpuset,attr"`
+}
+
+// DomainCPUPin represents a CPU pinning configuration.
+type DomainCPUPin struct {
+ CPUSet string `xml:"cpuset,attr"`
+}
+
+// DomainNumaTune represents NUMA tuning configuration.
+type DomainNumaTune struct {
+ Memory *DomainNumaMemory `xml:"memory,omitempty"`
+ MemNodes []DomainNumaMemNode `xml:"memnode,omitempty"`
+}
+
+// DomainNumaMemory represents NUMA memory configuration.
+type DomainNumaMemory struct {
+ Mode string `xml:"mode,attr"`
+ Nodeset string `xml:"nodeset,attr"`
+}
+
+// DomainNumaMemNode represents a NUMA memory node configuration.
+type DomainNumaMemNode struct {
+ CellID uint64 `xml:"cellid,attr"`
+ Mode string `xml:"mode,attr"`
+ Nodeset string `xml:"nodeset,attr"`
+}
+
+// DomainResource represents resource configuration.
+type DomainResource struct {
+ Partition string `xml:"partition,omitempty"`
+}
+
+// DomainOS represents OS configuration.
+type DomainOS struct {
+ Type *DomainOSType `xml:"type,omitempty"`
+ Kernel string `xml:"kernel,omitempty"`
+ Boot *DomainBoot `xml:"boot,omitempty"`
+}
+
+// DomainOSType represents the OS type.
+type DomainOSType struct {
+ Arch string `xml:"arch,attr"`
+ Value string `xml:",chardata"`
+}
+
+// DomainBoot represents boot configuration.
+type DomainBoot struct {
+ Dev string `xml:"dev,attr"`
+}
+
+// DomainCPU represents CPU configuration.
+type DomainCPU struct {
+ Mode string `xml:"mode,attr,omitempty"`
+ Topology *DomainCPUTopology `xml:"topology,omitempty"`
+ Numa *DomainCPUNuma `xml:"numa,omitempty"`
+}
+
+// DomainCPUTopology represents CPU topology.
+type DomainCPUTopology struct {
+ Sockets int `xml:"sockets,attr"`
+ Dies int `xml:"dies,attr"`
+ Clusters int `xml:"clusters,attr"`
+ Cores int `xml:"cores,attr"`
+ Threads int `xml:"threads,attr"`
+}
+
+// DomainCPUNuma represents CPU NUMA configuration.
+type DomainCPUNuma struct {
+ Cells []DomainCPUNumaCell `xml:"cell"`
+}
+
+// DomainCPUNumaCell represents a NUMA cell.
+type DomainCPUNumaCell struct {
+ ID uint64 `xml:"id,attr"`
+ CPUs string `xml:"cpus,attr"`
+ Memory uint64 `xml:"memory,attr"`
+ Unit string `xml:"unit,attr"`
+ MemAccess string `xml:"memAccess,attr,omitempty"`
+}
+
+// DomainClock represents clock configuration.
+type DomainClock struct {
+ Offset string `xml:"offset,attr"`
+}
+
+// DomainDevices represents all devices.
+type DomainDevices struct {
+ Emulator string `xml:"emulator,omitempty"`
+ Disks []DomainDisk `xml:"disk,omitempty"`
+ Interfaces []DomainInterface `xml:"interface,omitempty"`
+ Serials []DomainSerial `xml:"serial,omitempty"`
+}
+
+// DomainDisk represents a disk device.
+type DomainDisk struct {
+ Type string `xml:"type,attr"`
+ Device string `xml:"device,attr"`
+ Driver *DomainDiskDriver `xml:"driver,omitempty"`
+ Source *DomainDiskSource `xml:"source,omitempty"`
+ Target *DomainDiskTarget `xml:"target,omitempty"`
+ Alias *DomainAlias `xml:"alias,omitempty"`
+}
+
+// DomainDiskDriver represents disk driver configuration.
+type DomainDiskDriver struct {
+ Type string `xml:"type,attr"`
+ Cache string `xml:"cache,attr,omitempty"`
+ Discard string `xml:"discard,attr,omitempty"`
+}
+
+// DomainDiskSource represents disk source.
+type DomainDiskSource struct {
+ File string `xml:"file,attr,omitempty"`
+}
+
+// DomainDiskTarget represents disk target.
+type DomainDiskTarget struct {
+ Dev string `xml:"dev,attr"`
+ Bus string `xml:"bus,attr"`
+}
+
+// DomainInterface represents a network interface.
+type DomainInterface struct {
+ Type string `xml:"type,attr"`
+ MAC *DomainInterfaceMAC `xml:"mac,omitempty"`
+ Source *DomainInterfaceSource `xml:"source,omitempty"`
+ Target *DomainInterfaceTarget `xml:"target,omitempty"`
+ Model *DomainInterfaceModel `xml:"model,omitempty"`
+ Driver *DomainInterfaceDriver `xml:"driver,omitempty"`
+ MTU *DomainInterfaceMTU `xml:"mtu,omitempty"`
+ Alias *DomainAlias `xml:"alias,omitempty"`
+ Address *DomainAddress `xml:"address,omitempty"`
+}
+
+// DomainInterfaceMAC represents MAC address.
+type DomainInterfaceMAC struct {
+ Address string `xml:"address,attr"`
+}
+
+// DomainInterfaceSource represents network source.
+type DomainInterfaceSource struct {
+ Bridge string `xml:"bridge,attr,omitempty"`
+}
+
+// DomainInterfaceTarget represents network target.
+type DomainInterfaceTarget struct {
+ Dev string `xml:"dev,attr"`
+}
+
+// DomainInterfaceModel represents network model.
+type DomainInterfaceModel struct {
+ Type string `xml:"type,attr"`
+}
+
+// DomainInterfaceDriver represents network driver.
+type DomainInterfaceDriver struct {
+ Queues string `xml:"queues,attr,omitempty"`
+ Packed string `xml:"packed,attr,omitempty"`
+}
+
+// DomainInterfaceMTU represents MTU configuration.
+type DomainInterfaceMTU struct {
+ Size int `xml:"size,attr"`
+}
+
+// DomainSerial represents a serial device.
+type DomainSerial struct {
+ Type string `xml:"type,attr"`
+ Source *DomainSerialSource `xml:"source,omitempty"`
+ Protocol *DomainSerialProtocol `xml:"protocol,omitempty"`
+ Log *DomainSerialLog `xml:"log,omitempty"`
+ Target *DomainSerialTarget `xml:"target,omitempty"`
+}
+
+// DomainSerialSource represents serial source.
+type DomainSerialSource struct {
+ Mode string `xml:"mode,attr"`
+ Host string `xml:"host,attr"`
+ Service string `xml:"service,attr"`
+}
+
+// DomainSerialProtocol represents serial protocol.
+type DomainSerialProtocol struct {
+ Type string `xml:"type,attr"`
+}
+
+// DomainSerialLog represents serial log configuration.
+type DomainSerialLog struct {
+ File string `xml:"file,attr"`
+ Append string `xml:"append,attr"`
+}
+
+// DomainSerialTarget represents serial target.
+type DomainSerialTarget struct {
+ Port int `xml:"port,attr"`
+}
+
+// DomainAlias represents a device alias.
+type DomainAlias struct {
+ Name string `xml:"name,attr"`
+}
+
+// DomainAddress represents a device address.
+type DomainAddress struct {
+ Type string `xml:"type,attr"`
+ Domain string `xml:"domain,attr,omitempty"`
+ Bus string `xml:"bus,attr,omitempty"`
+ Slot string `xml:"slot,attr,omitempty"`
+ Function string `xml:"function,attr,omitempty"`
+}
diff --git a/internal/libvirt/dominfo/schema_test.go b/internal/libvirt/dominfo/schema_test.go
new file mode 100644
index 0000000..5f98e4c
--- /dev/null
+++ b/internal/libvirt/dominfo/schema_test.go
@@ -0,0 +1,568 @@
+/*
+SPDX-FileCopyrightText: Copyright 2025 SAP SE or an SAP affiliate company and cobaltcore-dev contributors
+SPDX-License-Identifier: Apache-2.0
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package dominfo
+
+import (
+ "encoding/xml"
+ "testing"
+)
+
+func TestDomainInfoDeserialization(t *testing.T) {
+ // Unmarshal the XML into our DomainInfo struct
+ var domainInfo DomainInfo
+ err := xml.Unmarshal(exampleXML, &domainInfo)
+ if err != nil {
+ t.Fatalf("Failed to unmarshal XML: %v", err)
+ }
+
+ // Verify domain type and basic attributes
+ if domainInfo.Type != "kvm" {
+ t.Errorf("Expected domain type to be 'kvm', got '%s'", domainInfo.Type)
+ }
+ if domainInfo.ID != "654321" {
+ t.Errorf("Expected domain ID to be '654321', got '%s'", domainInfo.ID)
+ }
+ if domainInfo.Name != "instance-12345-abc" {
+ t.Errorf("Expected domain name to be 'instance-12345-abc', got '%s'", domainInfo.Name)
+ }
+ if domainInfo.UUID != "12345-abc" {
+ t.Errorf("Expected domain UUID to be '12345-abc', got '%s'", domainInfo.UUID)
+ }
+
+ // Verify metadata section
+ if domainInfo.Metadata == nil {
+ t.Fatal("Expected metadata to be present")
+ }
+ if domainInfo.Metadata.NovaInstance == nil {
+ t.Fatal("Expected nova instance metadata to be present")
+ }
+
+ nova := domainInfo.Metadata.NovaInstance
+ if nova.Name != "example-12345-abc" {
+ t.Errorf("Expected nova name to be 'example-12345-abc', got '%s'", nova.Name)
+ }
+ if nova.CreationTime != "2025-12-18 00:49:23" {
+ t.Errorf("Expected creation time to be '2025-12-18 00:49:23', got '%s'", nova.CreationTime)
+ }
+
+ // Verify nova package
+ if nova.Package == nil {
+ t.Fatal("Expected nova package to be present")
+ }
+ if nova.Package.Version != "28.1.1" {
+ t.Errorf("Expected package version to be '28.1.1', got '%s'", nova.Package.Version)
+ }
+
+ // Verify nova flavor
+ if nova.Flavor == nil {
+ t.Fatal("Expected nova flavor to be present")
+ }
+ if nova.Flavor.Name != "g_k_c6_m24_v2" {
+ t.Errorf("Expected flavor name to be 'g_k_c6_m24_v2', got '%s'", nova.Flavor.Name)
+ }
+ if nova.Flavor.Memory != 24560 {
+ t.Errorf("Expected flavor memory to be 24560, got %d", nova.Flavor.Memory)
+ }
+ if nova.Flavor.VCPUs != 6 {
+ t.Errorf("Expected flavor vcpus to be 6, got %d", nova.Flavor.VCPUs)
+ }
+
+ // Verify nova owner
+ if nova.Owner == nil {
+ t.Fatal("Expected nova owner to be present")
+ }
+ if nova.Owner.User == nil {
+ t.Fatal("Expected nova user to be present")
+ }
+ if nova.Owner.User.UUID != "12345-abc" {
+ t.Errorf("Expected user UUID to be '12345-abc', got '%s'", nova.Owner.User.UUID)
+ }
+ if nova.Owner.User.Value != "example-user" {
+ t.Errorf("Expected user value to be 'example-user', got '%s'", nova.Owner.User.Value)
+ }
+ if nova.Owner.Project == nil {
+ t.Fatal("Expected nova project to be present")
+ }
+ if nova.Owner.Project.UUID != "12345-abc" {
+ t.Errorf("Expected project UUID to be '12345-abc', got '%s'", nova.Owner.Project.UUID)
+ }
+ if nova.Owner.Project.Value != "example-project" {
+ t.Errorf("Expected project value to be 'example-project', got '%s'", nova.Owner.Project.Value)
+ }
+
+ // Verify nova root
+ if nova.Root == nil {
+ t.Fatal("Expected nova root to be present")
+ }
+ if nova.Root.Type != "image" {
+ t.Errorf("Expected root type to be 'image', got '%s'", nova.Root.Type)
+ }
+ if nova.Root.UUID != "12345-abc" {
+ t.Errorf("Expected root UUID to be '12345-abc', got '%s'", nova.Root.UUID)
+ }
+
+ // Verify nova ports
+ if nova.Ports == nil {
+ t.Fatal("Expected nova ports to be present")
+ }
+ if len(nova.Ports.Ports) != 1 {
+ t.Fatalf("Expected 1 nova port, got %d", len(nova.Ports.Ports))
+ }
+ port := nova.Ports.Ports[0]
+ if port.UUID != "12345-abc" {
+ t.Errorf("Expected port UUID to be '12345-abc', got '%s'", port.UUID)
+ }
+ if len(port.IPs) != 1 {
+ t.Fatalf("Expected 1 IP address, got %d", len(port.IPs))
+ }
+ ip := port.IPs[0]
+ if ip.Type != "fixed" {
+ t.Errorf("Expected IP type to be 'fixed', got '%s'", ip.Type)
+ }
+ if ip.Address != "0.0.0.0" {
+ t.Errorf("Expected IP address to be '0.0.0.0', got '%s'", ip.Address)
+ }
+ if ip.IPVersion != "4" {
+ t.Errorf("Expected IP version to be '4', got '%s'", ip.IPVersion)
+ }
+
+ // Verify memory section
+ if domainInfo.Memory == nil {
+ t.Fatal("Expected memory to be present")
+ }
+ if domainInfo.Memory.Unit != "KiB" {
+ t.Errorf("Expected memory unit to be 'KiB', got '%s'", domainInfo.Memory.Unit)
+ }
+ if domainInfo.Memory.Value != 25149440 {
+ t.Errorf("Expected memory value to be 25149440, got %d", domainInfo.Memory.Value)
+ }
+
+ // Verify current memory
+ if domainInfo.CurrentMemory == nil {
+ t.Fatal("Expected current memory to be present")
+ }
+ if domainInfo.CurrentMemory.Value != 25149440 {
+ t.Errorf("Expected current memory value to be 25149440, got %d", domainInfo.CurrentMemory.Value)
+ }
+
+ // Verify memory backing
+ if domainInfo.MemoryBacking == nil {
+ t.Fatal("Expected memory backing to be present")
+ }
+ if domainInfo.MemoryBacking.HugePages == nil {
+ t.Fatal("Expected huge pages to be present")
+ }
+ if len(domainInfo.MemoryBacking.HugePages.Pages) != 1 {
+ t.Fatalf("Expected 1 huge page configuration, got %d", len(domainInfo.MemoryBacking.HugePages.Pages))
+ }
+ page := domainInfo.MemoryBacking.HugePages.Pages[0]
+ if page.Size != "2048" {
+ t.Errorf("Expected page size to be '2048', got '%s'", page.Size)
+ }
+ if page.Unit != "KiB" {
+ t.Errorf("Expected page unit to be 'KiB', got '%s'", page.Unit)
+ }
+ if page.Nodeset != "0" {
+ t.Errorf("Expected page nodeset to be '0', got '%s'", page.Nodeset)
+ }
+
+ // Verify VCPU
+ if domainInfo.VCPU == nil {
+ t.Fatal("Expected VCPU to be present")
+ }
+ if domainInfo.VCPU.Placement != "static" {
+ t.Errorf("Expected VCPU placement to be 'static', got '%s'", domainInfo.VCPU.Placement)
+ }
+ if domainInfo.VCPU.Value != 6 {
+ t.Errorf("Expected VCPU value to be 6, got %d", domainInfo.VCPU.Value)
+ }
+
+ // Verify CPU tune
+ if domainInfo.CPUTune == nil {
+ t.Fatal("Expected CPU tune to be present")
+ }
+ if len(domainInfo.CPUTune.VCPUPins) != 6 {
+ t.Fatalf("Expected 6 VCPU pins, got %d", len(domainInfo.CPUTune.VCPUPins))
+ }
+ vcpuPin := domainInfo.CPUTune.VCPUPins[0]
+ if vcpuPin.VCPU != 0 {
+ t.Errorf("Expected first VCPU pin to be for VCPU 0, got %d", vcpuPin.VCPU)
+ }
+ if vcpuPin.CPUSet != "32-63,160-191" {
+ t.Errorf("Expected first VCPU pin cpuset to be '32-63,160-191', got '%s'", vcpuPin.CPUSet)
+ }
+ if domainInfo.CPUTune.EmulatorPin == nil {
+ t.Fatal("Expected emulator pin to be present")
+ }
+ if domainInfo.CPUTune.EmulatorPin.CPUSet != "32-63,160-191" {
+ t.Errorf("Expected emulator pin cpuset to be '32-63,160-191', got '%s'", domainInfo.CPUTune.EmulatorPin.CPUSet)
+ }
+
+ // Verify NUMA tune
+ if domainInfo.NumaTune == nil {
+ t.Fatal("Expected NUMA tune to be present")
+ }
+ if domainInfo.NumaTune.Memory == nil {
+ t.Fatal("Expected NUMA memory to be present")
+ }
+ if domainInfo.NumaTune.Memory.Mode != "strict" {
+ t.Errorf("Expected NUMA memory mode to be 'strict', got '%s'", domainInfo.NumaTune.Memory.Mode)
+ }
+ if domainInfo.NumaTune.Memory.Nodeset != "1" {
+ t.Errorf("Expected NUMA memory nodeset to be '1', got '%s'", domainInfo.NumaTune.Memory.Nodeset)
+ }
+ if len(domainInfo.NumaTune.MemNodes) != 1 {
+ t.Fatalf("Expected 1 NUMA memory node, got %d", len(domainInfo.NumaTune.MemNodes))
+ }
+ memNode := domainInfo.NumaTune.MemNodes[0]
+ if memNode.CellID != 0 {
+ t.Errorf("Expected memory node cell ID to be 0, got %d", memNode.CellID)
+ }
+ if memNode.Mode != "strict" {
+ t.Errorf("Expected memory node mode to be 'strict', got '%s'", memNode.Mode)
+ }
+ if memNode.Nodeset != "1" {
+ t.Errorf("Expected memory node nodeset to be '1', got '%s'", memNode.Nodeset)
+ }
+
+ // Verify resource
+ if domainInfo.Resource == nil {
+ t.Fatal("Expected resource to be present")
+ }
+ if domainInfo.Resource.Partition != "/machine" {
+ t.Errorf("Expected resource partition to be '/machine', got '%s'", domainInfo.Resource.Partition)
+ }
+
+ // Verify OS
+ if domainInfo.OS == nil {
+ t.Fatal("Expected OS to be present")
+ }
+ if domainInfo.OS.Type == nil {
+ t.Fatal("Expected OS type to be present")
+ }
+ if domainInfo.OS.Type.Arch != "x86_64" {
+ t.Errorf("Expected OS arch to be 'x86_64', got '%s'", domainInfo.OS.Type.Arch)
+ }
+ if domainInfo.OS.Type.Value != "hvm" {
+ t.Errorf("Expected OS type value to be 'hvm', got '%s'", domainInfo.OS.Type.Value)
+ }
+ if domainInfo.OS.Kernel != "/usr/share/cloud-hypervisor/CLOUDHV_EFI.fd" {
+ t.Errorf("Expected OS kernel path, got '%s'", domainInfo.OS.Kernel)
+ }
+ if domainInfo.OS.Boot == nil {
+ t.Fatal("Expected OS boot to be present")
+ }
+ if domainInfo.OS.Boot.Dev != "hd" {
+ t.Errorf("Expected boot dev to be 'hd', got '%s'", domainInfo.OS.Boot.Dev)
+ }
+
+ // Verify CPU
+ if domainInfo.CPU == nil {
+ t.Fatal("Expected CPU to be present")
+ }
+ if domainInfo.CPU.Mode != "host-passthrough" {
+ t.Errorf("Expected CPU mode to be 'host-passthrough', got '%s'", domainInfo.CPU.Mode)
+ }
+ if domainInfo.CPU.Topology == nil {
+ t.Fatal("Expected CPU topology to be present")
+ }
+ topo := domainInfo.CPU.Topology
+ if topo.Sockets != 6 {
+ t.Errorf("Expected 6 sockets, got %d", topo.Sockets)
+ }
+ if topo.Dies != 1 {
+ t.Errorf("Expected 1 die, got %d", topo.Dies)
+ }
+ if topo.Clusters != 1 {
+ t.Errorf("Expected 1 cluster, got %d", topo.Clusters)
+ }
+ if topo.Cores != 1 {
+ t.Errorf("Expected 1 core, got %d", topo.Cores)
+ }
+ if topo.Threads != 1 {
+ t.Errorf("Expected 1 thread, got %d", topo.Threads)
+ }
+
+ // Verify CPU NUMA
+ if domainInfo.CPU.Numa == nil {
+ t.Fatal("Expected CPU NUMA to be present")
+ }
+ if len(domainInfo.CPU.Numa.Cells) != 1 {
+ t.Fatalf("Expected 1 NUMA cell, got %d", len(domainInfo.CPU.Numa.Cells))
+ }
+ cell := domainInfo.CPU.Numa.Cells[0]
+ if cell.ID != 0 {
+ t.Errorf("Expected cell ID to be 0, got %d", cell.ID)
+ }
+ if cell.CPUs != "0-5" {
+ t.Errorf("Expected cell CPUs to be '0-5', got '%s'", cell.CPUs)
+ }
+ if cell.Memory != 25149440 {
+ t.Errorf("Expected cell memory to be 25149440, got %d", cell.Memory)
+ }
+ if cell.Unit != "KiB" {
+ t.Errorf("Expected cell unit to be 'KiB', got '%s'", cell.Unit)
+ }
+ if cell.MemAccess != "shared" {
+ t.Errorf("Expected cell memAccess to be 'shared', got '%s'", cell.MemAccess)
+ }
+
+ // Verify clock
+ if domainInfo.Clock == nil {
+ t.Fatal("Expected clock to be present")
+ }
+ if domainInfo.Clock.Offset != "utc" {
+ t.Errorf("Expected clock offset to be 'utc', got '%s'", domainInfo.Clock.Offset)
+ }
+
+ // Verify lifecycle actions
+ if domainInfo.OnPoweroff != "destroy" {
+ t.Errorf("Expected on_poweroff to be 'destroy', got '%s'", domainInfo.OnPoweroff)
+ }
+ if domainInfo.OnReboot != "restart" {
+ t.Errorf("Expected on_reboot to be 'restart', got '%s'", domainInfo.OnReboot)
+ }
+ if domainInfo.OnCrash != "destroy" {
+ t.Errorf("Expected on_crash to be 'destroy', got '%s'", domainInfo.OnCrash)
+ }
+
+ // Verify devices
+ if domainInfo.Devices == nil {
+ t.Fatal("Expected devices to be present")
+ }
+ if domainInfo.Devices.Emulator != "/usr/bin/cloud-hypervisor" {
+ t.Errorf("Expected emulator to be '/usr/bin/cloud-hypervisor', got '%s'", domainInfo.Devices.Emulator)
+ }
+
+ // Verify disks
+ if len(domainInfo.Devices.Disks) != 1 {
+ t.Fatalf("Expected 1 disk, got %d", len(domainInfo.Devices.Disks))
+ }
+ disk := domainInfo.Devices.Disks[0]
+ if disk.Type != "file" {
+ t.Errorf("Expected disk type to be 'file', got '%s'", disk.Type)
+ }
+ if disk.Device != "disk" {
+ t.Errorf("Expected disk device to be 'disk', got '%s'", disk.Device)
+ }
+ if disk.Driver == nil {
+ t.Fatal("Expected disk driver to be present")
+ }
+ if disk.Driver.Type != "raw" {
+ t.Errorf("Expected disk driver type to be 'raw', got '%s'", disk.Driver.Type)
+ }
+ if disk.Driver.Cache != "none" {
+ t.Errorf("Expected disk driver cache to be 'none', got '%s'", disk.Driver.Cache)
+ }
+ if disk.Driver.Discard != "unmap" {
+ t.Errorf("Expected disk driver discard to be 'unmap', got '%s'", disk.Driver.Discard)
+ }
+ if disk.Source == nil {
+ t.Fatal("Expected disk source to be present")
+ }
+ if disk.Source.File != "/var/lib/nova/instances/12345-abc/disk" {
+ t.Errorf("Expected disk source file path, got '%s'", disk.Source.File)
+ }
+ if disk.Target == nil {
+ t.Fatal("Expected disk target to be present")
+ }
+ if disk.Target.Dev != "vda" {
+ t.Errorf("Expected disk target dev to be 'vda', got '%s'", disk.Target.Dev)
+ }
+ if disk.Target.Bus != "virtio" {
+ t.Errorf("Expected disk target bus to be 'virtio', got '%s'", disk.Target.Bus)
+ }
+ if disk.Alias == nil {
+ t.Fatal("Expected disk alias to be present")
+ }
+ if disk.Alias.Name != "virtio-disk0" {
+ t.Errorf("Expected disk alias to be 'virtio-disk0', got '%s'", disk.Alias.Name)
+ }
+
+ // Verify interfaces
+ if len(domainInfo.Devices.Interfaces) != 1 {
+ t.Fatalf("Expected 1 interface, got %d", len(domainInfo.Devices.Interfaces))
+ }
+ iface := domainInfo.Devices.Interfaces[0]
+ if iface.Type != "bridge" {
+ t.Errorf("Expected interface type to be 'bridge', got '%s'", iface.Type)
+ }
+ if iface.MAC == nil {
+ t.Fatal("Expected interface MAC to be present")
+ }
+ if iface.MAC.Address != "ab:cd:ef:12:34:56" {
+ t.Errorf("Expected MAC address to be 'ab:cd:ef:12:34:56', got '%s'", iface.MAC.Address)
+ }
+ if iface.Source == nil {
+ t.Fatal("Expected interface source to be present")
+ }
+ if iface.Source.Bridge != "abcdef" {
+ t.Errorf("Expected interface bridge to be 'abcdef', got '%s'", iface.Source.Bridge)
+ }
+ if iface.Target == nil {
+ t.Fatal("Expected interface target to be present")
+ }
+ if iface.Target.Dev != "abcdef" {
+ t.Errorf("Expected interface target dev to be 'abcdef', got '%s'", iface.Target.Dev)
+ }
+ if iface.Model == nil {
+ t.Fatal("Expected interface model to be present")
+ }
+ if iface.Model.Type != "virtio" {
+ t.Errorf("Expected interface model type to be 'virtio', got '%s'", iface.Model.Type)
+ }
+ if iface.Driver == nil {
+ t.Fatal("Expected interface driver to be present")
+ }
+ if iface.Driver.Queues != "1" {
+ t.Errorf("Expected interface driver queues to be '1', got '%s'", iface.Driver.Queues)
+ }
+ if iface.Driver.Packed != "on" {
+ t.Errorf("Expected interface driver packed to be 'on', got '%s'", iface.Driver.Packed)
+ }
+ if iface.MTU == nil {
+ t.Fatal("Expected interface MTU to be present")
+ }
+ if iface.MTU.Size != 8950 {
+ t.Errorf("Expected interface MTU size to be 8950, got %d", iface.MTU.Size)
+ }
+ if iface.Alias == nil {
+ t.Fatal("Expected interface alias to be present")
+ }
+ if iface.Alias.Name != "net_0" {
+ t.Errorf("Expected interface alias to be 'net_0', got '%s'", iface.Alias.Name)
+ }
+ if iface.Address == nil {
+ t.Fatal("Expected interface address to be present")
+ }
+ if iface.Address.Type != "pci" {
+ t.Errorf("Expected interface address type to be 'pci', got '%s'", iface.Address.Type)
+ }
+
+ // Verify serials
+ if len(domainInfo.Devices.Serials) != 1 {
+ t.Fatalf("Expected 1 serial, got %d", len(domainInfo.Devices.Serials))
+ }
+ serial := domainInfo.Devices.Serials[0]
+ if serial.Type != "tcp" {
+ t.Errorf("Expected serial type to be 'tcp', got '%s'", serial.Type)
+ }
+ if serial.Source == nil {
+ t.Fatal("Expected serial source to be present")
+ }
+ if serial.Source.Mode != "bind" {
+ t.Errorf("Expected serial source mode to be 'bind', got '%s'", serial.Source.Mode)
+ }
+ if serial.Source.Host != "10.245.239.50" {
+ t.Errorf("Expected serial source host to be '10.245.239.50', got '%s'", serial.Source.Host)
+ }
+ if serial.Source.Service != "10000" {
+ t.Errorf("Expected serial source service to be '10000', got '%s'", serial.Source.Service)
+ }
+ if serial.Protocol == nil {
+ t.Fatal("Expected serial protocol to be present")
+ }
+ if serial.Protocol.Type != "raw" {
+ t.Errorf("Expected serial protocol type to be 'raw', got '%s'", serial.Protocol.Type)
+ }
+ if serial.Log == nil {
+ t.Fatal("Expected serial log to be present")
+ }
+ if serial.Log.File != "/var/lib/nova/instances/12345-abc/console.log" {
+ t.Errorf("Expected serial log file path, got '%s'", serial.Log.File)
+ }
+ if serial.Log.Append != "off" {
+ t.Errorf("Expected serial log append to be 'off', got '%s'", serial.Log.Append)
+ }
+ if serial.Target == nil {
+ t.Fatal("Expected serial target to be present")
+ }
+ if serial.Target.Port != 0 {
+ t.Errorf("Expected serial target port to be 0, got %d", serial.Target.Port)
+ }
+}
+
+func TestDomainInfoRoundTrip(t *testing.T) {
+ // Unmarshal into struct
+ var domainInfo DomainInfo
+ err := xml.Unmarshal(exampleXML, &domainInfo)
+ if err != nil {
+ t.Fatalf("Failed to unmarshal XML: %v", err)
+ }
+
+ // Marshal back to XML
+ marshaledXML, err := xml.MarshalIndent(domainInfo, "", " ")
+ if err != nil {
+ t.Fatalf("Failed to marshal back to XML: %v", err)
+ }
+
+ // Unmarshal the marshaled XML to verify it's still valid
+ var roundTripDomainInfo DomainInfo
+ err = xml.Unmarshal(marshaledXML, &roundTripDomainInfo)
+ if err != nil {
+ t.Fatalf("Failed to unmarshal round-trip XML: %v", err)
+ }
+
+ // Verify key fields are preserved
+ if domainInfo.Type != roundTripDomainInfo.Type {
+ t.Errorf("Type mismatch after round trip: expected '%s', got '%s'",
+ domainInfo.Type, roundTripDomainInfo.Type)
+ }
+ if domainInfo.ID != roundTripDomainInfo.ID {
+ t.Errorf("ID mismatch after round trip: expected '%s', got '%s'",
+ domainInfo.ID, roundTripDomainInfo.ID)
+ }
+ if domainInfo.Name != roundTripDomainInfo.Name {
+ t.Errorf("Name mismatch after round trip: expected '%s', got '%s'",
+ domainInfo.Name, roundTripDomainInfo.Name)
+ }
+ if domainInfo.UUID != roundTripDomainInfo.UUID {
+ t.Errorf("UUID mismatch after round trip: expected '%s', got '%s'",
+ domainInfo.UUID, roundTripDomainInfo.UUID)
+ }
+
+ // Verify nested structures
+ if domainInfo.Memory != nil && roundTripDomainInfo.Memory != nil {
+ if domainInfo.Memory.Value != roundTripDomainInfo.Memory.Value {
+ t.Errorf("Memory value mismatch after round trip: expected %d, got %d",
+ domainInfo.Memory.Value, roundTripDomainInfo.Memory.Value)
+ }
+ }
+
+ if domainInfo.VCPU != nil && roundTripDomainInfo.VCPU != nil {
+ if domainInfo.VCPU.Value != roundTripDomainInfo.VCPU.Value {
+ t.Errorf("VCPU value mismatch after round trip: expected %d, got %d",
+ domainInfo.VCPU.Value, roundTripDomainInfo.VCPU.Value)
+ }
+ }
+
+ if domainInfo.Devices != nil && roundTripDomainInfo.Devices != nil {
+ if len(domainInfo.Devices.Disks) != len(roundTripDomainInfo.Devices.Disks) {
+ t.Errorf("Disks count mismatch after round trip: expected %d, got %d",
+ len(domainInfo.Devices.Disks), len(roundTripDomainInfo.Devices.Disks))
+ }
+ if len(domainInfo.Devices.Interfaces) != len(roundTripDomainInfo.Devices.Interfaces) {
+ t.Errorf("Interfaces count mismatch after round trip: expected %d, got %d",
+ len(domainInfo.Devices.Interfaces), len(roundTripDomainInfo.Devices.Interfaces))
+ }
+ if len(domainInfo.Devices.Serials) != len(roundTripDomainInfo.Devices.Serials) {
+ t.Errorf("Serials count mismatch after round trip: expected %d, got %d",
+ len(domainInfo.Devices.Serials), len(roundTripDomainInfo.Devices.Serials))
+ }
+ }
+}
diff --git a/internal/libvirt/interface.go b/internal/libvirt/interface.go
index 005fcfd..4a92e1d 100644
--- a/internal/libvirt/interface.go
+++ b/internal/libvirt/interface.go
@@ -21,7 +21,6 @@ package libvirt
import (
v1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
- "github.com/digitalocean/go-libvirt"
)
type Interface interface {
@@ -31,21 +30,8 @@ type Interface interface {
// Close closes the connection to the libvirt daemon.
Close() error
- // GetInstances returns a list of instances.
- GetInstances() ([]v1.Instance, error)
-
- // GetDomainsActive returns all active domains.
- GetDomainsActive() ([]libvirt.Domain, error)
-
- // IsConnected returns true if the connection to the libvirt daemon is open.
- IsConnected() bool
-
- // GetVersion returns the version of the libvirt daemon.
- GetVersion() string
-
- // GetNumInstances returns the number of instances.
- GetNumInstances() int
-
- // Get the capabilities of the libvirt daemon.
- GetCapabilities() (v1.CapabilitiesStatus, error)
+ // Add information extracted from the libvirt socket to the hypervisor instance.
+ // If an error occurs, the instance is returned unmodified. The libvirt
+ // connection needs to be established before calling this function.
+ Process(hv v1.Hypervisor) (v1.Hypervisor, error)
}
diff --git a/internal/libvirt/interface_mock.go b/internal/libvirt/interface_mock.go
index da7c464..06963d0 100644
--- a/internal/libvirt/interface_mock.go
+++ b/internal/libvirt/interface_mock.go
@@ -4,9 +4,9 @@
package libvirt
import (
- v1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
- "github.com/digitalocean/go-libvirt"
"sync"
+
+ v1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
)
// Ensure, that InterfaceMock does implement Interface.
@@ -25,23 +25,8 @@ var _ Interface = &InterfaceMock{}
// ConnectFunc: func() error {
// panic("mock out the Connect method")
// },
-// GetCapabilitiesFunc: func() (v1.CapabilitiesStatus, error) {
-// panic("mock out the GetCapabilities method")
-// },
-// GetDomainsActiveFunc: func() ([]libvirt.Domain, error) {
-// panic("mock out the GetDomainsActive method")
-// },
-// GetInstancesFunc: func() ([]v1.Instance, error) {
-// panic("mock out the GetInstances method")
-// },
-// GetNumInstancesFunc: func() int {
-// panic("mock out the GetNumInstances method")
-// },
-// GetVersionFunc: func() string {
-// panic("mock out the GetVersion method")
-// },
-// IsConnectedFunc: func() bool {
-// panic("mock out the IsConnected method")
+// ProcessFunc: func(hv v1.Hypervisor) (v1.Hypervisor, error) {
+// panic("mock out the Process method")
// },
// }
//
@@ -56,23 +41,8 @@ type InterfaceMock struct {
// ConnectFunc mocks the Connect method.
ConnectFunc func() error
- // GetCapabilitiesFunc mocks the GetCapabilities method.
- GetCapabilitiesFunc func() (v1.CapabilitiesStatus, error)
-
- // GetDomainsActiveFunc mocks the GetDomainsActive method.
- GetDomainsActiveFunc func() ([]libvirt.Domain, error)
-
- // GetInstancesFunc mocks the GetInstances method.
- GetInstancesFunc func() ([]v1.Instance, error)
-
- // GetNumInstancesFunc mocks the GetNumInstances method.
- GetNumInstancesFunc func() int
-
- // GetVersionFunc mocks the GetVersion method.
- GetVersionFunc func() string
-
- // IsConnectedFunc mocks the IsConnected method.
- IsConnectedFunc func() bool
+ // ProcessFunc mocks the Process method.
+ ProcessFunc func(hv v1.Hypervisor) (v1.Hypervisor, error)
// calls tracks calls to the methods.
calls struct {
@@ -82,33 +52,14 @@ type InterfaceMock struct {
// Connect holds details about calls to the Connect method.
Connect []struct {
}
- // GetCapabilities holds details about calls to the GetCapabilities method.
- GetCapabilities []struct {
- }
- // GetDomainsActive holds details about calls to the GetDomainsActive method.
- GetDomainsActive []struct {
- }
- // GetInstances holds details about calls to the GetInstances method.
- GetInstances []struct {
- }
- // GetNumInstances holds details about calls to the GetNumInstances method.
- GetNumInstances []struct {
- }
- // GetVersion holds details about calls to the GetVersion method.
- GetVersion []struct {
- }
- // IsConnected holds details about calls to the IsConnected method.
- IsConnected []struct {
+ // Process holds details about calls to the Process method.
+ Process []struct {
+ Hv v1.Hypervisor
}
}
- lockClose sync.RWMutex
- lockConnect sync.RWMutex
- lockGetCapabilities sync.RWMutex
- lockGetDomainsActive sync.RWMutex
- lockGetInstances sync.RWMutex
- lockGetNumInstances sync.RWMutex
- lockGetVersion sync.RWMutex
- lockIsConnected sync.RWMutex
+ lockClose sync.RWMutex
+ lockConnect sync.RWMutex
+ lockProcess sync.RWMutex
}
// Close calls CloseFunc.
@@ -165,164 +116,34 @@ func (mock *InterfaceMock) ConnectCalls() []struct {
return calls
}
-// GetCapabilities calls GetCapabilitiesFunc.
-func (mock *InterfaceMock) GetCapabilities() (v1.CapabilitiesStatus, error) {
- if mock.GetCapabilitiesFunc == nil {
- panic("InterfaceMock.GetCapabilitiesFunc: method is nil but Interface.GetCapabilities was just called")
- }
- callInfo := struct {
- }{}
- mock.lockGetCapabilities.Lock()
- mock.calls.GetCapabilities = append(mock.calls.GetCapabilities, callInfo)
- mock.lockGetCapabilities.Unlock()
- return mock.GetCapabilitiesFunc()
-}
-
-// GetCapabilitiesCalls gets all the calls that were made to GetCapabilities.
-// Check the length with:
-//
-// len(mockedInterface.GetCapabilitiesCalls())
-func (mock *InterfaceMock) GetCapabilitiesCalls() []struct {
-} {
- var calls []struct {
- }
- mock.lockGetCapabilities.RLock()
- calls = mock.calls.GetCapabilities
- mock.lockGetCapabilities.RUnlock()
- return calls
-}
-
-// GetDomainsActive calls GetDomainsActiveFunc.
-func (mock *InterfaceMock) GetDomainsActive() ([]libvirt.Domain, error) {
- if mock.GetDomainsActiveFunc == nil {
- panic("InterfaceMock.GetDomainsActiveFunc: method is nil but Interface.GetDomainsActive was just called")
+// Process calls ProcessFunc.
+func (mock *InterfaceMock) Process(hv v1.Hypervisor) (v1.Hypervisor, error) {
+ if mock.ProcessFunc == nil {
+ panic("InterfaceMock.ProcessFunc: method is nil but Interface.Process was just called")
}
callInfo := struct {
- }{}
- mock.lockGetDomainsActive.Lock()
- mock.calls.GetDomainsActive = append(mock.calls.GetDomainsActive, callInfo)
- mock.lockGetDomainsActive.Unlock()
- return mock.GetDomainsActiveFunc()
-}
-
-// GetDomainsActiveCalls gets all the calls that were made to GetDomainsActive.
-// Check the length with:
-//
-// len(mockedInterface.GetDomainsActiveCalls())
-func (mock *InterfaceMock) GetDomainsActiveCalls() []struct {
-} {
- var calls []struct {
- }
- mock.lockGetDomainsActive.RLock()
- calls = mock.calls.GetDomainsActive
- mock.lockGetDomainsActive.RUnlock()
- return calls
-}
-
-// GetInstances calls GetInstancesFunc.
-func (mock *InterfaceMock) GetInstances() ([]v1.Instance, error) {
- if mock.GetInstancesFunc == nil {
- panic("InterfaceMock.GetInstancesFunc: method is nil but Interface.GetInstances was just called")
+ Hv v1.Hypervisor
+ }{
+ Hv: hv,
}
- callInfo := struct {
- }{}
- mock.lockGetInstances.Lock()
- mock.calls.GetInstances = append(mock.calls.GetInstances, callInfo)
- mock.lockGetInstances.Unlock()
- return mock.GetInstancesFunc()
-}
-
-// GetInstancesCalls gets all the calls that were made to GetInstances.
-// Check the length with:
-//
-// len(mockedInterface.GetInstancesCalls())
-func (mock *InterfaceMock) GetInstancesCalls() []struct {
-} {
- var calls []struct {
- }
- mock.lockGetInstances.RLock()
- calls = mock.calls.GetInstances
- mock.lockGetInstances.RUnlock()
- return calls
-}
-
-// GetNumInstances calls GetNumInstancesFunc.
-func (mock *InterfaceMock) GetNumInstances() int {
- if mock.GetNumInstancesFunc == nil {
- panic("InterfaceMock.GetNumInstancesFunc: method is nil but Interface.GetNumInstances was just called")
- }
- callInfo := struct {
- }{}
- mock.lockGetNumInstances.Lock()
- mock.calls.GetNumInstances = append(mock.calls.GetNumInstances, callInfo)
- mock.lockGetNumInstances.Unlock()
- return mock.GetNumInstancesFunc()
-}
-
-// GetNumInstancesCalls gets all the calls that were made to GetNumInstances.
-// Check the length with:
-//
-// len(mockedInterface.GetNumInstancesCalls())
-func (mock *InterfaceMock) GetNumInstancesCalls() []struct {
-} {
- var calls []struct {
- }
- mock.lockGetNumInstances.RLock()
- calls = mock.calls.GetNumInstances
- mock.lockGetNumInstances.RUnlock()
- return calls
-}
-
-// GetVersion calls GetVersionFunc.
-func (mock *InterfaceMock) GetVersion() string {
- if mock.GetVersionFunc == nil {
- panic("InterfaceMock.GetVersionFunc: method is nil but Interface.GetVersion was just called")
- }
- callInfo := struct {
- }{}
- mock.lockGetVersion.Lock()
- mock.calls.GetVersion = append(mock.calls.GetVersion, callInfo)
- mock.lockGetVersion.Unlock()
- return mock.GetVersionFunc()
-}
-
-// GetVersionCalls gets all the calls that were made to GetVersion.
-// Check the length with:
-//
-// len(mockedInterface.GetVersionCalls())
-func (mock *InterfaceMock) GetVersionCalls() []struct {
-} {
- var calls []struct {
- }
- mock.lockGetVersion.RLock()
- calls = mock.calls.GetVersion
- mock.lockGetVersion.RUnlock()
- return calls
-}
-
-// IsConnected calls IsConnectedFunc.
-func (mock *InterfaceMock) IsConnected() bool {
- if mock.IsConnectedFunc == nil {
- panic("InterfaceMock.IsConnectedFunc: method is nil but Interface.IsConnected was just called")
- }
- callInfo := struct {
- }{}
- mock.lockIsConnected.Lock()
- mock.calls.IsConnected = append(mock.calls.IsConnected, callInfo)
- mock.lockIsConnected.Unlock()
- return mock.IsConnectedFunc()
+ mock.lockProcess.Lock()
+ mock.calls.Process = append(mock.calls.Process, callInfo)
+ mock.lockProcess.Unlock()
+ return mock.ProcessFunc(hv)
}
-// IsConnectedCalls gets all the calls that were made to IsConnected.
+// ProcessCalls gets all the calls that were made to Process.
// Check the length with:
//
-// len(mockedInterface.IsConnectedCalls())
-func (mock *InterfaceMock) IsConnectedCalls() []struct {
+// len(mockedInterface.ProcessCalls())
+func (mock *InterfaceMock) ProcessCalls() []struct {
+ Hv v1.Hypervisor
} {
var calls []struct {
+ Hv v1.Hypervisor
}
- mock.lockIsConnected.RLock()
- calls = mock.calls.IsConnected
- mock.lockIsConnected.RUnlock()
+ mock.lockProcess.RLock()
+ calls = mock.calls.Process
+ mock.lockProcess.RUnlock()
return calls
}
diff --git a/internal/libvirt/libvirt.go b/internal/libvirt/libvirt.go
index 9a319e4..048a5dd 100644
--- a/internal/libvirt/libvirt.go
+++ b/internal/libvirt/libvirt.go
@@ -27,10 +27,13 @@ import (
v1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
"github.com/digitalocean/go-libvirt"
"github.com/digitalocean/go-libvirt/socket/dialers"
+ "k8s.io/apimachinery/pkg/api/resource"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
"github.com/cobaltcore-dev/kvm-node-agent/internal/libvirt/capabilities"
+ "github.com/cobaltcore-dev/kvm-node-agent/internal/libvirt/domcapabilities"
+ "github.com/cobaltcore-dev/kvm-node-agent/internal/libvirt/dominfo"
)
type LibVirt struct {
@@ -41,8 +44,16 @@ type LibVirt struct {
version string
domains map[libvirt.ConnectListAllDomainsFlags][]libvirt.Domain
- // Client that connects to libvirt and fetches capabilities of the hypervisor.
+ // Client that connects to libvirt and fetches capabilities of the
+ // hypervisor. The capabilities client abstracts the xml parsing away.
capabilitiesClient capabilities.Client
+ // Client that connects to libvirt and fetches domain capabilities
+ // of the hypervisor. The domain capabilities client abstracts the
+ // xml parsing away.
+ domainCapabilitiesClient domcapabilities.Client
+ // Client that connects to libvirt and fetches domain information.
+ // The domain information client abstracts the xml parsing away.
+ domainInfoClient dominfo.Client
}
func NewLibVirt(k client.Client) *LibVirt {
@@ -64,6 +75,8 @@ func NewLibVirt(k client.Client) *LibVirt {
"N/A",
make(map[libvirt.ConnectListAllDomainsFlags][]libvirt.Domain, 2),
capabilities.NewClient(),
+ domcapabilities.NewClient(),
+ dominfo.NewClient(),
}
}
@@ -99,15 +112,42 @@ func (l *LibVirt) Connect() error {
return err
}
-func (l *LibVirt) GetVersion() string {
- return l.version
-}
-
func (l *LibVirt) Close() error {
return l.virt.Disconnect()
}
-func (l *LibVirt) GetInstances() ([]v1.Instance, error) {
+// Add information extracted from the libvirt socket to the hypervisor instance.
+// If an error occurs, the instance is returned unmodified. The libvirt
+// connection needs to be established before calling this function.
+func (l *LibVirt) Process(hv v1.Hypervisor) (v1.Hypervisor, error) {
+ processors := []func(v1.Hypervisor) (v1.Hypervisor, error){
+ l.addVersion,
+ l.addInstancesInfo,
+ l.addCapabilities,
+ l.addDomainCapabilities,
+ l.addAllocationCapacity,
+ }
+ var err error
+ for _, processor := range processors {
+ if hv, err = processor(hv); err != nil {
+ log.Log.Error(err, "failed to process hypervisor", "step", processor)
+ return hv, err
+ }
+ }
+ return hv, nil
+}
+
+// Add the libvirt version to the hypervisor instance.
+func (l *LibVirt) addVersion(old v1.Hypervisor) (v1.Hypervisor, error) {
+ newHv := old
+ newHv.Status.LibVirtVersion = l.version
+ return newHv, nil
+}
+
+// Add the domain flags to the hypervisor instance, i.e. how many
+// instances are running and how many are inactive.
+func (l *LibVirt) addInstancesInfo(old v1.Hypervisor) (v1.Hypervisor, error) {
+ newHv := old
var instances []v1.Instance
flags := []libvirt.ConnectListAllDomainsFlags{libvirt.ConnectListDomainsActive, libvirt.ConnectListDomainsInactive}
@@ -120,22 +160,229 @@ func (l *LibVirt) GetInstances() ([]v1.Instance, error) {
})
}
}
- return instances, nil
-}
-func (l *LibVirt) GetDomainsActive() ([]libvirt.Domain, error) {
- return l.domains[libvirt.ConnectListDomainsActive], nil
+ newHv.Status.Instances = instances
+ newHv.Status.NumInstances = len(l.domains)
+ return newHv, nil
}
-func (l *LibVirt) IsConnected() bool {
- return l.virt.IsConnected()
+// Call the libvirt capabilities API and add the resulting information
+// to the hypervisor capabilities status.
+func (l *LibVirt) addCapabilities(old v1.Hypervisor) (v1.Hypervisor, error) {
+ newHv := old
+ caps, err := l.capabilitiesClient.Get(l.virt)
+ if err != nil {
+ return old, err
+ }
+ newHv.Status.Capabilities.HostCpuArch = caps.Host.CPU.Arch
+ // Loop over all numa cells to get the total memory + vcpus capacity.
+ totalMemory := resource.NewQuantity(0, resource.BinarySI)
+ totalCpus := resource.NewQuantity(0, resource.DecimalSI)
+ for _, cell := range caps.Host.Topology.CellSpec.Cells {
+ mem, err := MemoryToResource(cell.Memory.Value, cell.Memory.Unit)
+ if err != nil {
+ return old, err
+ }
+ totalMemory.Add(mem)
+ cpu := resource.NewQuantity(cell.CPUs.Num, resource.DecimalSI)
+ if cpu == nil {
+ return old, fmt.Errorf("invalid CPU count for cell %d", cell.ID)
+ }
+ totalCpus.Add(*cpu)
+ }
+ newHv.Status.Capabilities.HostMemory = *totalMemory
+ newHv.Status.Capabilities.HostCpus = *totalCpus
+ return newHv, nil
}
-func (l *LibVirt) GetNumInstances() int {
- return len(l.domains[libvirt.ConnectListDomainsActive])
+// Call the libvirt domcapabilities api and add the resulting information
+// to the hypervisor domain capabilities status.
+func (l *LibVirt) addDomainCapabilities(old v1.Hypervisor) (v1.Hypervisor, error) {
+ newHv := old
+ domCapabilities, err := l.domainCapabilitiesClient.Get(l.virt)
+ if err != nil {
+ return old, err
+ }
+
+ newHv.Status.DomainCapabilities.Arch = domCapabilities.Arch
+ newHv.Status.DomainCapabilities.HypervisorType = domCapabilities.Domain
+
+ // Convert the supported cpu modes into a flat list of supported cpu types.
+ // - becomes
+ // "mode/example" and "mode/example/1"
+ // - is ignored
+ // - becomes "mode/example3"
+ newHv.Status.DomainCapabilities.SupportedCpuModes = []string{}
+ for _, cpuMode := range domCapabilities.CPU.Modes {
+ if cpuMode.Supported != "yes" {
+ continue
+ }
+ newHv.Status.DomainCapabilities.SupportedCpuModes = append(
+ newHv.Status.DomainCapabilities.SupportedCpuModes,
+ "mode/"+cpuMode.Name,
+ )
+ for _, enum := range cpuMode.Enums {
+ for _, cpuType := range enum.Values {
+ newHv.Status.DomainCapabilities.SupportedCpuModes = append(
+ newHv.Status.DomainCapabilities.SupportedCpuModes,
+ fmt.Sprintf("mode/%s/%s", cpuMode.Name, cpuType),
+ )
+ }
+ }
+ }
+
+ // Convert the supported devices into a flat list.
+ // -
+ // becomes "video" and "video/v1"
+ // - is ignored
+ // - becomes "video".
+ newHv.Status.DomainCapabilities.SupportedDevices = []string{}
+ for _, device := range domCapabilities.Devices.Devices {
+ if device.Supported != "yes" {
+ continue
+ }
+ newHv.Status.DomainCapabilities.SupportedDevices = append(
+ newHv.Status.DomainCapabilities.SupportedDevices,
+ device.XMLName.Local,
+ )
+ for _, enum := range device.Enums {
+ for _, deviceType := range enum.Values {
+ newHv.Status.DomainCapabilities.SupportedDevices = append(
+ newHv.Status.DomainCapabilities.SupportedDevices,
+ fmt.Sprintf("%s/%s", device.XMLName.Local, deviceType),
+ )
+ }
+ }
+ }
+
+ // Convert the supported features into a flat list.
+ newHv.Status.DomainCapabilities.SupportedFeatures = []string{}
+ for _, feature := range domCapabilities.Features.Features {
+ if feature.Supported == "yes" {
+ newHv.Status.DomainCapabilities.SupportedFeatures = append(
+ newHv.Status.DomainCapabilities.SupportedFeatures,
+ feature.XMLName.Local,
+ )
+ }
+ }
+
+ return newHv, nil
}
-// Get the capabilities of the libvirt daemon.
-func (l *LibVirt) GetCapabilities() (v1.CapabilitiesStatus, error) {
- return l.capabilitiesClient.Get(l.virt)
+// Add total allocation, total capacity, and numa cell information
+// to the hypervisor instance, by combining domain infos and hypervisor
+// capabilities in libvirt.
+func (l *LibVirt) addAllocationCapacity(old v1.Hypervisor) (v1.Hypervisor, error) {
+ newHv := old
+
+ // First get all the numa cells from the capabilities
+ caps, err := l.capabilitiesClient.Get(l.virt)
+ if err != nil {
+ return old, err
+ }
+ totalMemoryCapacity := resource.NewQuantity(0, resource.BinarySI)
+ totalCpuCapacity := resource.NewQuantity(0, resource.DecimalSI)
+ cellsById := make(map[uint64]v1.Cell)
+ for _, cell := range caps.Host.Topology.CellSpec.Cells {
+ memoryCapacity, err := MemoryToResource(
+ cell.Memory.Value,
+ cell.Memory.Unit,
+ )
+ if err != nil {
+ return old, err
+ }
+ totalMemoryCapacity.Add(memoryCapacity)
+
+ cpuCapacity := *resource.NewQuantity(
+ cell.CPUs.Num,
+ resource.DecimalSI,
+ )
+ totalCpuCapacity.Add(cpuCapacity)
+
+ cellsById[cell.ID] = v1.Cell{
+ CellID: cell.ID,
+ Allocation: map[string]resource.Quantity{
+ // Will be updated below when we look at the domain infos.
+ "memory": *resource.NewQuantity(0, resource.BinarySI),
+ "cpu": *resource.NewQuantity(0, resource.DecimalSI),
+ },
+ Capacity: map[string]resource.Quantity{
+ "memory": memoryCapacity,
+ "cpu": cpuCapacity,
+ },
+ }
+ }
+
+ // Now get all domain infos to calculate the total allocation.
+ domInfos, err := l.domainInfoClient.Get(l.virt)
+ if err != nil {
+ return old, err
+ }
+ totalMemoryAlloc := resource.NewQuantity(0, resource.BinarySI)
+ totalCpuAlloc := resource.NewQuantity(0, resource.DecimalSI)
+ for _, domInfo := range domInfos {
+ memAlloc, err := MemoryToResource(
+ domInfo.Memory.Value,
+ domInfo.Memory.Unit,
+ )
+ if err != nil {
+ return old, err
+ }
+ totalMemoryAlloc.Add(memAlloc)
+
+ if domInfo.CPUTune == nil {
+ return old, fmt.Errorf("missing cpu tune for dom %s", domInfo.Name)
+ }
+ cpuAlloc := *resource.NewQuantity(
+ int64(len(domInfo.CPUTune.VCPUPins)),
+ resource.DecimalSI,
+ )
+ totalCpuAlloc.Add(cpuAlloc)
+
+ // Add memory allocation to the cells this domain is using.
+ for _, memoryNode := range domInfo.NumaTune.MemNodes {
+ cell, ok := cellsById[memoryNode.CellID]
+ if !ok {
+ return old, fmt.Errorf(
+ "domain %s uses unknown memory cell %d",
+ domInfo.Name, memoryNode.CellID,
+ )
+ }
+ memAllocCell := cell.Allocation["memory"]
+ memAllocCell.Add(memAlloc)
+ cell.Allocation["memory"] = memAllocCell
+ cellsById[memoryNode.CellID] = cell
+ }
+
+ // Add cpu allocation to the cells this domain is using.
+ if domInfo.CPU.Numa == nil {
+ return old, fmt.Errorf("missing cpu numa for dom %s", domInfo.Name)
+ }
+ for _, cpuCell := range domInfo.CPU.Numa.Cells {
+ cell, ok := cellsById[cpuCell.ID]
+ if !ok {
+ return old, fmt.Errorf(
+ "domain %s uses unknown cpu cell %d",
+ domInfo.Name, cpuCell.ID,
+ )
+ }
+ cpuAllocCell := cell.Allocation["cpu"]
+ cpuAllocCell.Add(cpuAlloc)
+ cell.Allocation["cpu"] = cpuAllocCell
+ cellsById[cpuCell.ID] = cell
+ }
+ }
+ cellsAsSlice := []v1.Cell{}
+ for _, cell := range cellsById {
+ cellsAsSlice = append(cellsAsSlice, cell)
+ }
+
+ newHv.Status.Capacity = make(map[string]resource.Quantity)
+ newHv.Status.Capacity["memory"] = *totalMemoryCapacity
+ newHv.Status.Capacity["cpu"] = *totalCpuCapacity
+ newHv.Status.Allocation = make(map[string]resource.Quantity)
+ newHv.Status.Allocation["memory"] = *totalMemoryAlloc
+ newHv.Status.Allocation["cpu"] = *totalCpuAlloc
+ newHv.Status.Cells = cellsAsSlice
+ return newHv, nil
}
diff --git a/internal/libvirt/libvirt_test.go b/internal/libvirt/libvirt_test.go
new file mode 100644
index 0000000..68f0558
--- /dev/null
+++ b/internal/libvirt/libvirt_test.go
@@ -0,0 +1,655 @@
+/*
+SPDX-FileCopyrightText: Copyright 2025 SAP SE or an SAP affiliate company and cobaltcore-dev contributors
+SPDX-License-Identifier: Apache-2.0
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package libvirt
+
+import (
+ "testing"
+
+ v1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
+ libvirt "github.com/digitalocean/go-libvirt"
+ "k8s.io/apimachinery/pkg/api/resource"
+
+ "github.com/cobaltcore-dev/kvm-node-agent/internal/libvirt/capabilities"
+ "github.com/cobaltcore-dev/kvm-node-agent/internal/libvirt/domcapabilities"
+ "github.com/cobaltcore-dev/kvm-node-agent/internal/libvirt/dominfo"
+)
+
+// mockCapabilitiesClient implements the capabilities.Client interface for testing
+type mockCapabilitiesClient struct {
+ caps capabilities.Capabilities
+ err error
+}
+
+func (m *mockCapabilitiesClient) Get(virt *libvirt.Libvirt) (capabilities.Capabilities, error) {
+ if m.err != nil {
+ return capabilities.Capabilities{}, m.err
+ }
+ return m.caps, nil
+}
+
+// mockDomCapabilitiesClient implements the domcapabilities.Client interface for testing
+type mockDomCapabilitiesClient struct {
+ caps domcapabilities.DomainCapabilities
+ err error
+}
+
+func (m *mockDomCapabilitiesClient) Get(virt *libvirt.Libvirt) (domcapabilities.DomainCapabilities, error) {
+ if m.err != nil {
+ return domcapabilities.DomainCapabilities{}, m.err
+ }
+ return m.caps, nil
+}
+
+// mockDomInfoClient implements the dominfo.Client interface for testing
+type mockDomInfoClient struct {
+ infos []dominfo.DomainInfo
+ err error
+}
+
+func (m *mockDomInfoClient) Get(virt *libvirt.Libvirt) ([]dominfo.DomainInfo, error) {
+ if m.err != nil {
+ return nil, m.err
+ }
+ return m.infos, nil
+}
+
+func TestAddVersion(t *testing.T) {
+ l := &LibVirt{
+ version: "8.0.0",
+ }
+
+ hv := v1.Hypervisor{}
+ result, err := l.addVersion(hv)
+
+ if err != nil {
+ t.Fatalf("addVersion() returned unexpected error: %v", err)
+ }
+
+ if result.Status.LibVirtVersion != "8.0.0" {
+ t.Errorf("Expected LibVirtVersion '8.0.0', got '%s'", result.Status.LibVirtVersion)
+ }
+}
+
+func TestAddVersion_PreservesOtherFields(t *testing.T) {
+ l := &LibVirt{
+ version: "8.0.0",
+ }
+
+ hv := v1.Hypervisor{
+ Status: v1.HypervisorStatus{
+ NumInstances: 5,
+ },
+ }
+
+ result, err := l.addVersion(hv)
+
+ if err != nil {
+ t.Fatalf("addVersion() returned unexpected error: %v", err)
+ }
+
+ if result.Status.NumInstances != 5 {
+ t.Errorf("Expected NumInstances to be preserved, got %d", result.Status.NumInstances)
+ }
+}
+
+func TestAddInstancesInfo_ActiveDomains(t *testing.T) {
+ l := &LibVirt{
+ domains: map[libvirt.ConnectListAllDomainsFlags][]libvirt.Domain{
+ libvirt.ConnectListDomainsActive: {
+ {Name: "instance-1", UUID: [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}},
+ {Name: "instance-2", UUID: [16]byte{2, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}},
+ },
+ libvirt.ConnectListDomainsInactive: {},
+ },
+ }
+
+ hv := v1.Hypervisor{}
+ result, err := l.addInstancesInfo(hv)
+
+ if err != nil {
+ t.Fatalf("addInstancesInfo() returned unexpected error: %v", err)
+ }
+
+ if len(result.Status.Instances) != 2 {
+ t.Fatalf("Expected 2 instances, got %d", len(result.Status.Instances))
+ }
+
+ // Check that both instances are active
+ for _, instance := range result.Status.Instances {
+ if !instance.Active {
+ t.Errorf("Expected instance '%s' to be active", instance.Name)
+ }
+ }
+}
+
+func TestAddInstancesInfo_InactiveDomains(t *testing.T) {
+ l := &LibVirt{
+ domains: map[libvirt.ConnectListAllDomainsFlags][]libvirt.Domain{
+ libvirt.ConnectListDomainsActive: {},
+ libvirt.ConnectListDomainsInactive: {
+ {Name: "instance-3", UUID: [16]byte{3, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}},
+ },
+ },
+ }
+
+ hv := v1.Hypervisor{}
+ result, err := l.addInstancesInfo(hv)
+
+ if err != nil {
+ t.Fatalf("addInstancesInfo() returned unexpected error: %v", err)
+ }
+
+ if len(result.Status.Instances) != 1 {
+ t.Fatalf("Expected 1 instance, got %d", len(result.Status.Instances))
+ }
+
+ if result.Status.Instances[0].Active {
+ t.Error("Expected instance to be inactive")
+ }
+}
+
+func TestAddInstancesInfo_MixedDomains(t *testing.T) {
+ l := &LibVirt{
+ domains: map[libvirt.ConnectListAllDomainsFlags][]libvirt.Domain{
+ libvirt.ConnectListDomainsActive: {
+ {Name: "active-1"},
+ {Name: "active-2"},
+ },
+ libvirt.ConnectListDomainsInactive: {
+ {Name: "inactive-1"},
+ },
+ },
+ }
+
+ hv := v1.Hypervisor{}
+ result, err := l.addInstancesInfo(hv)
+
+ if err != nil {
+ t.Fatalf("addInstancesInfo() returned unexpected error: %v", err)
+ }
+
+ if len(result.Status.Instances) != 3 {
+ t.Fatalf("Expected 3 instances, got %d", len(result.Status.Instances))
+ }
+
+ // Count active and inactive
+ activeCount := 0
+ inactiveCount := 0
+ for _, instance := range result.Status.Instances {
+ if instance.Active {
+ activeCount++
+ } else {
+ inactiveCount++
+ }
+ }
+
+ if activeCount != 2 {
+ t.Errorf("Expected 2 active instances, got %d", activeCount)
+ }
+ if inactiveCount != 1 {
+ t.Errorf("Expected 1 inactive instance, got %d", inactiveCount)
+ }
+}
+
+func TestAddCapabilities_Success(t *testing.T) {
+ caps := capabilities.Capabilities{
+ Host: capabilities.CapabilitiesHost{
+ CPU: capabilities.CapabilitiesHostCPU{
+ Arch: "x86_64",
+ },
+ Topology: capabilities.CapabilitiesHostTopology{
+ CellSpec: capabilities.CapabilitiesHostTopologyCells{
+ Num: 1,
+ Cells: []capabilities.CapabilitiesHostTopologyCell{
+ {
+ ID: 0,
+ Memory: capabilities.CapabilitiesHostTopologyCellMemory{
+ Unit: "KiB",
+ Value: 16777216, // 16 GiB in KiB
+ },
+ CPUs: capabilities.CapabilitiesHostTopologyCellCPUs{
+ Num: 8,
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ l := &LibVirt{
+ capabilitiesClient: &mockCapabilitiesClient{caps: caps},
+ }
+
+ hv := v1.Hypervisor{}
+ result, err := l.addCapabilities(hv)
+
+ if err != nil {
+ t.Fatalf("addCapabilities() returned unexpected error: %v", err)
+ }
+
+ if result.Status.Capabilities.HostCpuArch != "x86_64" {
+ t.Errorf("Expected HostCpuArch 'x86_64', got '%s'", result.Status.Capabilities.HostCpuArch)
+ }
+
+ expectedMemory := resource.NewQuantity(16777216*1024, resource.BinarySI)
+ if !result.Status.Capabilities.HostMemory.Equal(*expectedMemory) {
+ t.Errorf("Expected HostMemory %s, got %s", expectedMemory.String(), result.Status.Capabilities.HostMemory.String())
+ }
+
+ expectedCpus := resource.NewQuantity(8, resource.DecimalSI)
+ if !result.Status.Capabilities.HostCpus.Equal(*expectedCpus) {
+ t.Errorf("Expected HostCpus %s, got %s", expectedCpus.String(), result.Status.Capabilities.HostCpus.String())
+ }
+}
+
+func TestAddCapabilities_MultipleCells(t *testing.T) {
+ caps := capabilities.Capabilities{
+ Host: capabilities.CapabilitiesHost{
+ CPU: capabilities.CapabilitiesHostCPU{
+ Arch: "x86_64",
+ },
+ Topology: capabilities.CapabilitiesHostTopology{
+ CellSpec: capabilities.CapabilitiesHostTopologyCells{
+ Num: 2,
+ Cells: []capabilities.CapabilitiesHostTopologyCell{
+ {
+ ID: 0,
+ Memory: capabilities.CapabilitiesHostTopologyCellMemory{
+ Unit: "GiB",
+ Value: 32,
+ },
+ CPUs: capabilities.CapabilitiesHostTopologyCellCPUs{
+ Num: 16,
+ },
+ },
+ {
+ ID: 1,
+ Memory: capabilities.CapabilitiesHostTopologyCellMemory{
+ Unit: "GiB",
+ Value: 32,
+ },
+ CPUs: capabilities.CapabilitiesHostTopologyCellCPUs{
+ Num: 16,
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ l := &LibVirt{
+ capabilitiesClient: &mockCapabilitiesClient{caps: caps},
+ }
+
+ hv := v1.Hypervisor{}
+ result, err := l.addCapabilities(hv)
+
+ if err != nil {
+ t.Fatalf("addCapabilities() returned unexpected error: %v", err)
+ }
+
+ // Total should be 64 GiB
+ expectedMemory := resource.NewQuantity(64*1024*1024*1024, resource.BinarySI)
+ if !result.Status.Capabilities.HostMemory.Equal(*expectedMemory) {
+ t.Errorf("Expected HostMemory %s, got %s", expectedMemory.String(), result.Status.Capabilities.HostMemory.String())
+ }
+
+ // Total should be 32 CPUs
+ expectedCpus := resource.NewQuantity(32, resource.DecimalSI)
+ if !result.Status.Capabilities.HostCpus.Equal(*expectedCpus) {
+ t.Errorf("Expected HostCpus %s, got %s", expectedCpus.String(), result.Status.Capabilities.HostCpus.String())
+ }
+}
+
+func TestAddDomainCapabilities_Success(t *testing.T) {
+ domCaps := domcapabilities.DomainCapabilities{
+ Domain: "kvm",
+ Arch: "x86_64",
+ CPU: domcapabilities.DomainCapabilitiesCPU{
+ Modes: []domcapabilities.DomainCapabilitiesCPUMode{
+ {
+ Name: "host-passthrough",
+ Supported: "yes",
+ },
+ {
+ Name: "custom",
+ Supported: "yes",
+ Enums: []domcapabilities.DomainCapabilitiesEnum{
+ {
+ Name: "model",
+ Values: []string{"Skylake-Client", "Broadwell"},
+ },
+ },
+ },
+ },
+ },
+ Devices: domcapabilities.DomainCapabilitiesDevices{
+ Devices: []domcapabilities.DomainCapabilitiesDevice{
+ {
+ Supported: "yes",
+ Enums: []domcapabilities.DomainCapabilitiesEnum{
+ {
+ Name: "type",
+ Values: []string{"vnc", "spice"},
+ },
+ },
+ },
+ },
+ },
+ Features: domcapabilities.DomainCapabilitiesFeatures{
+ Features: []domcapabilities.DomainCapabilitiesFeature{
+ {
+ Supported: "yes",
+ },
+ },
+ },
+ }
+ // Set XMLName for device
+ domCaps.Devices.Devices[0].XMLName.Local = "graphics"
+ // Set XMLName for feature
+ domCaps.Features.Features[0].XMLName.Local = "acpi"
+
+ l := &LibVirt{
+ domainCapabilitiesClient: &mockDomCapabilitiesClient{caps: domCaps},
+ }
+
+ hv := v1.Hypervisor{}
+ result, err := l.addDomainCapabilities(hv)
+
+ if err != nil {
+ t.Fatalf("addDomainCapabilities() returned unexpected error: %v", err)
+ }
+
+ if result.Status.DomainCapabilities.Arch != "x86_64" {
+ t.Errorf("Expected Arch 'x86_64', got '%s'", result.Status.DomainCapabilities.Arch)
+ }
+
+ if result.Status.DomainCapabilities.HypervisorType != "kvm" {
+ t.Errorf("Expected HypervisorType 'kvm', got '%s'", result.Status.DomainCapabilities.HypervisorType)
+ }
+
+ // Check CPU modes
+ expectedCpuModes := []string{
+ "mode/host-passthrough",
+ "mode/custom",
+ "mode/custom/Skylake-Client",
+ "mode/custom/Broadwell",
+ }
+ if len(result.Status.DomainCapabilities.SupportedCpuModes) != len(expectedCpuModes) {
+ t.Errorf("Expected %d CPU modes, got %d", len(expectedCpuModes), len(result.Status.DomainCapabilities.SupportedCpuModes))
+ }
+
+ // Check devices
+ expectedDevices := []string{
+ "graphics",
+ "graphics/vnc",
+ "graphics/spice",
+ }
+ if len(result.Status.DomainCapabilities.SupportedDevices) != len(expectedDevices) {
+ t.Errorf("Expected %d devices, got %d", len(expectedDevices), len(result.Status.DomainCapabilities.SupportedDevices))
+ }
+
+ // Check features
+ if len(result.Status.DomainCapabilities.SupportedFeatures) != 1 {
+ t.Errorf("Expected 1 feature, got %d", len(result.Status.DomainCapabilities.SupportedFeatures))
+ }
+ if result.Status.DomainCapabilities.SupportedFeatures[0] != "acpi" {
+ t.Errorf("Expected feature 'acpi', got '%s'", result.Status.DomainCapabilities.SupportedFeatures[0])
+ }
+}
+
+func TestAddDomainCapabilities_UnsupportedFiltered(t *testing.T) {
+ domCaps := domcapabilities.DomainCapabilities{
+ Domain: "kvm",
+ Arch: "x86_64",
+ CPU: domcapabilities.DomainCapabilitiesCPU{
+ Modes: []domcapabilities.DomainCapabilitiesCPUMode{
+ {
+ Name: "supported-mode",
+ Supported: "yes",
+ },
+ {
+ Name: "unsupported-mode",
+ Supported: "no",
+ },
+ },
+ },
+ }
+
+ l := &LibVirt{
+ domainCapabilitiesClient: &mockDomCapabilitiesClient{caps: domCaps},
+ }
+
+ hv := v1.Hypervisor{}
+ result, err := l.addDomainCapabilities(hv)
+
+ if err != nil {
+ t.Fatalf("addDomainCapabilities() returned unexpected error: %v", err)
+ }
+
+ // Only the supported mode should be included
+ if len(result.Status.DomainCapabilities.SupportedCpuModes) != 1 {
+ t.Errorf("Expected 1 supported CPU mode, got %d", len(result.Status.DomainCapabilities.SupportedCpuModes))
+ }
+
+ if result.Status.DomainCapabilities.SupportedCpuModes[0] != "mode/supported-mode" {
+ t.Errorf("Expected 'mode/supported-mode', got '%s'", result.Status.DomainCapabilities.SupportedCpuModes[0])
+ }
+}
+
+func TestAddAllocationCapacity_Success(t *testing.T) {
+ caps := capabilities.Capabilities{
+ Host: capabilities.CapabilitiesHost{
+ Topology: capabilities.CapabilitiesHostTopology{
+ CellSpec: capabilities.CapabilitiesHostTopologyCells{
+ Num: 1,
+ Cells: []capabilities.CapabilitiesHostTopologyCell{
+ {
+ ID: 0,
+ Memory: capabilities.CapabilitiesHostTopologyCellMemory{
+ Unit: "GiB",
+ Value: 64,
+ },
+ CPUs: capabilities.CapabilitiesHostTopologyCellCPUs{
+ Num: 16,
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ domInfos := []dominfo.DomainInfo{
+ {
+ Name: "test-instance",
+ Memory: &dominfo.DomainMemory{
+ Unit: "GiB",
+ Value: 8,
+ },
+ CPUTune: &dominfo.DomainCPUTune{
+ VCPUPins: []dominfo.DomainVCPUPin{
+ {VCPU: 0, CPUSet: "0"},
+ {VCPU: 1, CPUSet: "1"},
+ },
+ },
+ NumaTune: &dominfo.DomainNumaTune{
+ MemNodes: []dominfo.DomainNumaMemNode{
+ {CellID: 0, Mode: "strict", Nodeset: "0"},
+ },
+ },
+ CPU: &dominfo.DomainCPU{
+ Numa: &dominfo.DomainCPUNuma{
+ Cells: []dominfo.DomainCPUNumaCell{
+ {ID: 0, CPUs: "0-1", Memory: 8, Unit: "GiB"},
+ },
+ },
+ },
+ },
+ }
+
+ l := &LibVirt{
+ capabilitiesClient: &mockCapabilitiesClient{caps: caps},
+ domainInfoClient: &mockDomInfoClient{infos: domInfos},
+ }
+
+ hv := v1.Hypervisor{}
+ result, err := l.addAllocationCapacity(hv)
+
+ if err != nil {
+ t.Fatalf("addAllocationCapacity() returned unexpected error: %v", err)
+ }
+
+ // Check total capacity
+ expectedMemCapacity := resource.NewQuantity(64*1024*1024*1024, resource.BinarySI)
+ memCap := result.Status.Capacity["memory"]
+ if !memCap.Equal(*expectedMemCapacity) {
+ t.Errorf("Expected memory capacity %s, got %s",
+ expectedMemCapacity.String(), memCap.String())
+ }
+
+ expectedCpuCapacity := resource.NewQuantity(16, resource.DecimalSI)
+ cpuCap := result.Status.Capacity["cpu"]
+ if !cpuCap.Equal(*expectedCpuCapacity) {
+ t.Errorf("Expected CPU capacity %s, got %s",
+ expectedCpuCapacity.String(), cpuCap.String())
+ }
+
+ // Check total allocation
+ expectedMemAlloc := resource.NewQuantity(8*1024*1024*1024, resource.BinarySI)
+ memAlloc := result.Status.Allocation["memory"]
+ if !memAlloc.Equal(*expectedMemAlloc) {
+ t.Errorf("Expected memory allocation %s, got %s",
+ expectedMemAlloc.String(), memAlloc.String())
+ }
+
+ expectedCpuAlloc := resource.NewQuantity(2, resource.DecimalSI)
+ cpuAlloc := result.Status.Allocation["cpu"]
+ if !cpuAlloc.Equal(*expectedCpuAlloc) {
+ t.Errorf("Expected CPU allocation %s, got %s",
+ expectedCpuAlloc.String(), cpuAlloc.String())
+ }
+
+ // Check cells
+ if len(result.Status.Cells) != 1 {
+ t.Fatalf("Expected 1 cell, got %d", len(result.Status.Cells))
+ }
+}
+
+func TestProcess_Success(t *testing.T) {
+ caps := capabilities.Capabilities{
+ Host: capabilities.CapabilitiesHost{
+ CPU: capabilities.CapabilitiesHostCPU{
+ Arch: "x86_64",
+ },
+ Topology: capabilities.CapabilitiesHostTopology{
+ CellSpec: capabilities.CapabilitiesHostTopologyCells{
+ Num: 1,
+ Cells: []capabilities.CapabilitiesHostTopologyCell{
+ {
+ ID: 0,
+ Memory: capabilities.CapabilitiesHostTopologyCellMemory{
+ Unit: "GiB",
+ Value: 16,
+ },
+ CPUs: capabilities.CapabilitiesHostTopologyCellCPUs{
+ Num: 4,
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ domCaps := domcapabilities.DomainCapabilities{
+ Domain: "kvm",
+ Arch: "x86_64",
+ CPU: domcapabilities.DomainCapabilitiesCPU{
+ Modes: []domcapabilities.DomainCapabilitiesCPUMode{},
+ },
+ }
+
+ l := &LibVirt{
+ version: "8.0.0",
+ domains: make(map[libvirt.ConnectListAllDomainsFlags][]libvirt.Domain),
+ capabilitiesClient: &mockCapabilitiesClient{caps: caps},
+ domainCapabilitiesClient: &mockDomCapabilitiesClient{caps: domCaps},
+ domainInfoClient: &mockDomInfoClient{infos: []dominfo.DomainInfo{}},
+ }
+
+ hv := v1.Hypervisor{}
+ result, err := l.Process(hv)
+
+ if err != nil {
+ t.Fatalf("Process() returned unexpected error: %v", err)
+ }
+
+ // Verify all processors ran
+ if result.Status.LibVirtVersion != "8.0.0" {
+ t.Error("addVersion did not run")
+ }
+ if result.Status.Capabilities.HostCpuArch != "x86_64" {
+ t.Error("addCapabilities did not run")
+ }
+ if result.Status.DomainCapabilities.HypervisorType != "kvm" {
+ t.Error("addDomainCapabilities did not run")
+ }
+ if result.Status.Capacity == nil {
+ t.Error("addAllocationCapacity did not run")
+ }
+}
+
+func TestProcess_PreservesOriginalOnError(t *testing.T) {
+ l := &LibVirt{
+ version: "8.0.0",
+ domains: make(map[libvirt.ConnectListAllDomainsFlags][]libvirt.Domain),
+ capabilitiesClient: &mockCapabilitiesClient{err: &testError{"capability error"}},
+ domainCapabilitiesClient: &mockDomCapabilitiesClient{},
+ domainInfoClient: &mockDomInfoClient{},
+ }
+
+ originalHv := v1.Hypervisor{
+ Status: v1.HypervisorStatus{
+ NumInstances: 42,
+ },
+ }
+
+ result, err := l.Process(originalHv)
+
+ if err == nil {
+ t.Fatal("Expected error from Process(), got nil")
+ }
+
+ // The hypervisor should be returned even on error
+ // Version should have been added before the error
+ if result.Status.LibVirtVersion != "8.0.0" {
+ t.Error("Expected version to be added before error")
+ }
+}
+
+// testError is a simple error type for testing
+type testError struct {
+ msg string
+}
+
+func (e *testError) Error() string {
+ return e.msg
+}
diff --git a/internal/libvirt/utils.go b/internal/libvirt/utils.go
index 85a71a8..40aa713 100644
--- a/internal/libvirt/utils.go
+++ b/internal/libvirt/utils.go
@@ -22,6 +22,7 @@ import (
"fmt"
"github.com/digitalocean/go-libvirt"
+ "k8s.io/apimachinery/pkg/api/resource"
)
type UUID [16]byte
@@ -57,3 +58,24 @@ func ByteCountIEC(b uint64) string {
return fmt.Sprintf("%.1f %ciB",
float64(b)/float64(div), "KMGTPE"[exp])
}
+
+// Get the cell memory as resource.Quantity.
+func MemoryToResource(value int64, unit string) (resource.Quantity, error) {
+ var quantity *resource.Quantity
+ // Check the unit
+ switch unit {
+ case "KiB":
+ quantity = resource.NewQuantity(value*1024, resource.BinarySI)
+ case "MiB":
+ quantity = resource.NewQuantity(value*1024*1024, resource.BinarySI)
+ case "GiB":
+ quantity = resource.NewQuantity(value*1024*1024*1024, resource.BinarySI)
+ case "TiB":
+ quantity = resource.NewQuantity(value*1024*1024*1024*1024, resource.BinarySI)
+ }
+ if quantity == nil {
+ return resource.Quantity{}, fmt.Errorf("unknown memory unit %s", unit)
+ }
+ // Set the value
+ return *quantity, nil
+}
diff --git a/internal/libvirt/utils_test.go b/internal/libvirt/utils_test.go
new file mode 100644
index 0000000..20e90a3
--- /dev/null
+++ b/internal/libvirt/utils_test.go
@@ -0,0 +1,422 @@
+/*
+SPDX-FileCopyrightText: Copyright 2025 SAP SE or an SAP affiliate company and cobaltcore-dev contributors
+SPDX-License-Identifier: Apache-2.0
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package libvirt
+
+import (
+ "testing"
+
+ "k8s.io/apimachinery/pkg/api/resource"
+)
+
+func TestMemoryToResourceKiB(t *testing.T) {
+ tests := []struct {
+ name string
+ value int64
+ unit string
+ expectedBytes int64
+ }{
+ {
+ name: "1 KiB",
+ value: 1,
+ unit: "KiB",
+ expectedBytes: 1024,
+ },
+ {
+ name: "1024 KiB (1 MiB)",
+ value: 1024,
+ unit: "KiB",
+ expectedBytes: 1024 * 1024,
+ },
+ {
+ name: "Zero KiB",
+ value: 0,
+ unit: "KiB",
+ expectedBytes: 0,
+ },
+ {
+ name: "Large value KiB",
+ value: 1048576, // 1 GiB in KiB
+ unit: "KiB",
+ expectedBytes: 1024 * 1024 * 1024,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := MemoryToResource(tt.value, tt.unit)
+ if err != nil {
+ t.Fatalf("MemoryToResource() returned unexpected error: %v", err)
+ }
+
+ expectedQuantity := resource.NewQuantity(tt.expectedBytes, resource.BinarySI)
+ if !result.Equal(*expectedQuantity) {
+ t.Errorf("Expected quantity %s, got %s", expectedQuantity.String(), result.String())
+ }
+
+ // Verify the value in bytes
+ resultBytes, ok := result.AsInt64()
+ if !ok {
+ t.Fatal("Failed to convert result to int64")
+ }
+ if resultBytes != tt.expectedBytes {
+ t.Errorf("Expected %d bytes, got %d bytes", tt.expectedBytes, resultBytes)
+ }
+ })
+ }
+}
+
+func TestMemoryToResourceMiB(t *testing.T) {
+ tests := []struct {
+ name string
+ value int64
+ unit string
+ expectedBytes int64
+ }{
+ {
+ name: "1 MiB",
+ value: 1,
+ unit: "MiB",
+ expectedBytes: 1024 * 1024,
+ },
+ {
+ name: "1024 MiB (1 GiB)",
+ value: 1024,
+ unit: "MiB",
+ expectedBytes: 1024 * 1024 * 1024,
+ },
+ {
+ name: "Zero MiB",
+ value: 0,
+ unit: "MiB",
+ expectedBytes: 0,
+ },
+ {
+ name: "Large value MiB",
+ value: 16384, // 16 GiB in MiB
+ unit: "MiB",
+ expectedBytes: 16 * 1024 * 1024 * 1024,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := MemoryToResource(tt.value, tt.unit)
+ if err != nil {
+ t.Fatalf("MemoryToResource() returned unexpected error: %v", err)
+ }
+
+ expectedQuantity := resource.NewQuantity(tt.expectedBytes, resource.BinarySI)
+ if !result.Equal(*expectedQuantity) {
+ t.Errorf("Expected quantity %s, got %s", expectedQuantity.String(), result.String())
+ }
+
+ // Verify the value in bytes
+ resultBytes, ok := result.AsInt64()
+ if !ok {
+ t.Fatal("Failed to convert result to int64")
+ }
+ if resultBytes != tt.expectedBytes {
+ t.Errorf("Expected %d bytes, got %d bytes", tt.expectedBytes, resultBytes)
+ }
+ })
+ }
+}
+
+func TestMemoryToResourceGiB(t *testing.T) {
+ tests := []struct {
+ name string
+ value int64
+ unit string
+ expectedBytes int64
+ }{
+ {
+ name: "1 GiB",
+ value: 1,
+ unit: "GiB",
+ expectedBytes: 1024 * 1024 * 1024,
+ },
+ {
+ name: "8 GiB",
+ value: 8,
+ unit: "GiB",
+ expectedBytes: 8 * 1024 * 1024 * 1024,
+ },
+ {
+ name: "Zero GiB",
+ value: 0,
+ unit: "GiB",
+ expectedBytes: 0,
+ },
+ {
+ name: "Large value GiB",
+ value: 128,
+ unit: "GiB",
+ expectedBytes: 128 * 1024 * 1024 * 1024,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := MemoryToResource(tt.value, tt.unit)
+ if err != nil {
+ t.Fatalf("MemoryToResource() returned unexpected error: %v", err)
+ }
+
+ expectedQuantity := resource.NewQuantity(tt.expectedBytes, resource.BinarySI)
+ if !result.Equal(*expectedQuantity) {
+ t.Errorf("Expected quantity %s, got %s", expectedQuantity.String(), result.String())
+ }
+
+ // Verify the value in bytes
+ resultBytes, ok := result.AsInt64()
+ if !ok {
+ t.Fatal("Failed to convert result to int64")
+ }
+ if resultBytes != tt.expectedBytes {
+ t.Errorf("Expected %d bytes, got %d bytes", tt.expectedBytes, resultBytes)
+ }
+ })
+ }
+}
+
+func TestMemoryToResourceTiB(t *testing.T) {
+ tests := []struct {
+ name string
+ value int64
+ unit string
+ expectedBytes int64
+ }{
+ {
+ name: "1 TiB",
+ value: 1,
+ unit: "TiB",
+ expectedBytes: 1024 * 1024 * 1024 * 1024,
+ },
+ {
+ name: "2 TiB",
+ value: 2,
+ unit: "TiB",
+ expectedBytes: 2 * 1024 * 1024 * 1024 * 1024,
+ },
+ {
+ name: "Zero TiB",
+ value: 0,
+ unit: "TiB",
+ expectedBytes: 0,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := MemoryToResource(tt.value, tt.unit)
+ if err != nil {
+ t.Fatalf("MemoryToResource() returned unexpected error: %v", err)
+ }
+
+ expectedQuantity := resource.NewQuantity(tt.expectedBytes, resource.BinarySI)
+ if !result.Equal(*expectedQuantity) {
+ t.Errorf("Expected quantity %s, got %s", expectedQuantity.String(), result.String())
+ }
+
+ // Verify the value in bytes
+ resultBytes, ok := result.AsInt64()
+ if !ok {
+ t.Fatal("Failed to convert result to int64")
+ }
+ if resultBytes != tt.expectedBytes {
+ t.Errorf("Expected %d bytes, got %d bytes", tt.expectedBytes, resultBytes)
+ }
+ })
+ }
+}
+
+func TestMemoryToResourceInvalidUnit(t *testing.T) {
+ tests := []struct {
+ name string
+ value int64
+ unit string
+ }{
+ {
+ name: "Invalid unit KB",
+ value: 1024,
+ unit: "KB",
+ },
+ {
+ name: "Invalid unit MB",
+ value: 1024,
+ unit: "MB",
+ },
+ {
+ name: "Invalid unit GB",
+ value: 1024,
+ unit: "GB",
+ },
+ {
+ name: "Invalid unit TB",
+ value: 1024,
+ unit: "TB",
+ },
+ {
+ name: "Invalid unit bytes",
+ value: 1024,
+ unit: "bytes",
+ },
+ {
+ name: "Empty unit",
+ value: 1024,
+ unit: "",
+ },
+ {
+ name: "Random string",
+ value: 1024,
+ unit: "invalid",
+ },
+ {
+ name: "Case sensitive - kib",
+ value: 1024,
+ unit: "kib",
+ },
+ {
+ name: "Case sensitive - mib",
+ value: 1024,
+ unit: "mib",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := MemoryToResource(tt.value, tt.unit)
+ if err == nil {
+ t.Errorf("Expected error for invalid unit '%s', but got result: %s", tt.unit, result.String())
+ }
+
+ expectedError := "unknown memory unit " + tt.unit
+ if err.Error() != expectedError {
+ t.Errorf("Expected error message '%s', got '%s'", expectedError, err.Error())
+ }
+
+ // Verify the result is an empty Quantity
+ if !result.IsZero() {
+ t.Errorf("Expected zero quantity for error case, got %s", result.String())
+ }
+ })
+ }
+}
+
+func TestMemoryToResourceBinaryFormat(t *testing.T) {
+ // Verify that the returned quantities use BinarySI format
+ result, err := MemoryToResource(1, "GiB")
+ if err != nil {
+ t.Fatalf("MemoryToResource() returned unexpected error: %v", err)
+ }
+
+ // BinarySI should format as "1Gi" not "1073741824"
+ resultString := result.String()
+ if resultString != "1Gi" {
+ t.Errorf("Expected BinarySI format '1Gi', got '%s'", resultString)
+ }
+}
+
+func TestMemoryToResourceRealWorldScenarios(t *testing.T) {
+ tests := []struct {
+ name string
+ value int64
+ unit string
+ expectedStr string
+ description string
+ }{
+ {
+ name: "Typical VM memory - 8GB",
+ value: 8192,
+ unit: "MiB",
+ expectedStr: "8Gi",
+ description: "8GB RAM for a typical VM",
+ },
+ {
+ name: "Large VM memory - 64GB",
+ value: 64,
+ unit: "GiB",
+ expectedStr: "64Gi",
+ description: "64GB RAM for a large VM",
+ },
+ {
+ name: "Memory from example XML - ~24GB",
+ value: 25149440,
+ unit: "KiB",
+ expectedStr: "24560Mi",
+ description: "Memory value from the example domain XML",
+ },
+ {
+ name: "Small container memory - 512MB",
+ value: 524288,
+ unit: "KiB",
+ expectedStr: "512Mi",
+ description: "512MB memory for a small container",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := MemoryToResource(tt.value, tt.unit)
+ if err != nil {
+ t.Fatalf("MemoryToResource() returned unexpected error: %v", err)
+ }
+
+ resultString := result.String()
+ if resultString != tt.expectedStr {
+ t.Errorf("Expected '%s', got '%s' for %s", tt.expectedStr, resultString, tt.description)
+ }
+ })
+ }
+}
+
+func TestMemoryToResourceNegativeValues(t *testing.T) {
+ // Test behavior with negative values (edge case)
+ tests := []struct {
+ name string
+ value int64
+ unit string
+ }{
+ {
+ name: "Negative KiB",
+ value: -1024,
+ unit: "KiB",
+ },
+ {
+ name: "Negative MiB",
+ value: -512,
+ unit: "MiB",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // The function doesn't explicitly check for negative values,
+ // so it will create a negative quantity
+ result, err := MemoryToResource(tt.value, tt.unit)
+ if err != nil {
+ t.Fatalf("MemoryToResource() returned unexpected error: %v", err)
+ }
+
+ // Verify it's negative
+ if result.Sign() >= 0 {
+ t.Errorf("Expected negative quantity for negative input, got %s", result.String())
+ }
+ })
+ }
+}