Skip to content

Commit 5fe9e7d

Browse files
authored
Merge pull request #25 from cobaltcore-dev/capabilities-self-discovery
Expose cpu arch, number of cpus, and total memory
2 parents 10109d5 + 9a4b272 commit 5fe9e7d

File tree

15 files changed

+1629
-6
lines changed

15 files changed

+1629
-6
lines changed

.golangci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ linters:
1010
- dupl
1111
- errcheck
1212
- ginkgolinter
13-
- goconst
1413
- govet
1514
- ineffassign
1615
- lll

api/v1alpha1/hypervisor_types.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ limitations under the License.
1818
package v1alpha1
1919

2020
import (
21+
"k8s.io/apimachinery/pkg/api/resource"
2122
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2223
"k8s.io/apimachinery/pkg/types"
2324
)
@@ -106,6 +107,17 @@ type OperatingSystemStatus struct {
106107
FirmwareDate metav1.Time `json:"firmwareDate,omitempty"`
107108
}
108109

110+
// Current capabilities reported by libvirt.
111+
type CapabilitiesStatus struct {
112+
// +kubebuilder:default:=unknown
113+
// The hosts CPU architecture (not the guests).
114+
HostCpuArch string `json:"cpuArch,omitempty"`
115+
// Total host memory available as a sum of memory over all numa cells.
116+
HostMemory resource.Quantity `json:"memory,omitempty"`
117+
// Total host cpus available as a sum of cpus over all numa cells.
118+
HostCpus resource.Quantity `json:"cpus,omitempty"`
119+
}
120+
109121
// HypervisorStatus defines the observed state of Hypervisor
110122
type HypervisorStatus struct {
111123
// +kubebuilder:default:=unknown
@@ -124,6 +136,9 @@ type HypervisorStatus struct {
124136
// Represents the Hypervisor hosted Virtual Machines
125137
Instances []Instance `json:"instances,omitempty"`
126138

139+
// The capabilities of the hypervisors as reported by libvirt.
140+
Capabilities CapabilitiesStatus `json:"capabilities,omitempty"`
141+
127142
// +kubebuilder:default:=0
128143
// Represent the num of instances
129144
NumInstances int `json:"numInstances"`

config/crd/bases/kvm.cloud.sap_hypervisors.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,30 @@ spec:
8888
status:
8989
description: HypervisorStatus defines the observed state of Hypervisor
9090
properties:
91+
capabilities:
92+
description: The capabilities of the hypervisors as reported by libvirt.
93+
properties:
94+
cpuArch:
95+
default: unknown
96+
description: The hosts CPU architecture (not the guests).
97+
type: string
98+
cpus:
99+
anyOf:
100+
- type: integer
101+
- type: string
102+
description: Total host cpus available as a sum of cpus over all
103+
numa cells.
104+
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
105+
x-kubernetes-int-or-string: true
106+
memory:
107+
anyOf:
108+
- type: integer
109+
- type: string
110+
description: Total host memory available as a sum of memory over
111+
all numa cells.
112+
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
113+
x-kubernetes-int-or-string: true
114+
type: object
91115
conditions:
92116
description: Represents the Hypervisor node conditions.
93117
items:

internal/controller/hypervisor_controller.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ type HypervisorReconciler struct {
5252
}
5353

5454
const (
55-
OSUpdateType = "OperatingSystemUpdate"
56-
LibVirtType = "LibVirtConnection"
55+
OSUpdateType = "OperatingSystemUpdate"
56+
LibVirtType = "LibVirtConnection"
57+
CapabilitiesClientType = "CapabilitiesClientConnection"
5758
)
5859

5960
// +kubebuilder:rbac:groups=kvm.cloud.sap,resources=hypervisors,verbs=get;list;watch;create;update;patch;delete
@@ -131,6 +132,24 @@ func (r *HypervisorReconciler) Reconcile(ctx context.Context, req ctrl.Request)
131132
// Update hypervisor instances
132133
hypervisor.Status.NumInstances = r.Libvirt.GetNumInstances()
133134
hypervisor.Status.Instances, _ = r.Libvirt.GetInstances()
135+
136+
// Update capabilities status.
137+
if capabilities, err := r.Libvirt.GetCapabilities(); err == nil {
138+
hypervisor.Status.Capabilities = capabilities
139+
meta.SetStatusCondition(&hypervisor.Status.Conditions, metav1.Condition{
140+
Type: CapabilitiesClientType,
141+
Status: metav1.ConditionTrue,
142+
Reason: "CapabilitiesClientGetSucceeded",
143+
})
144+
} else {
145+
log.Error(err, "failed to get capabilities")
146+
meta.SetStatusCondition(&hypervisor.Status.Conditions, metav1.Condition{
147+
Type: CapabilitiesClientType,
148+
Status: metav1.ConditionFalse,
149+
Message: err.Error(),
150+
Reason: "CapabilitiesClientGetFailed",
151+
})
152+
}
134153
}
135154

136155
// ====================================================================================================

internal/controller/hypervisor_controller_test.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
. "github.com/onsi/ginkgo/v2"
2626
. "github.com/onsi/gomega"
2727
"k8s.io/apimachinery/pkg/api/errors"
28+
"k8s.io/apimachinery/pkg/api/resource"
2829
"k8s.io/apimachinery/pkg/types"
2930
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3031

@@ -108,6 +109,13 @@ var _ = Describe("Hypervisor Controller", func() {
108109
GetNumInstancesFunc: func() int {
109110
return 1
110111
},
112+
GetCapabilitiesFunc: func() (kvmv1alpha1.CapabilitiesStatus, error) {
113+
return kvmv1alpha1.CapabilitiesStatus{
114+
HostCpuArch: "x86_64",
115+
HostCpus: *resource.NewQuantity(4, resource.DecimalSI),
116+
HostMemory: *resource.NewQuantity(8192, resource.DecimalSI),
117+
}, nil
118+
},
111119
},
112120
Systemd: &systemd.InterfaceMock{
113121
CloseFunc: func() {},
@@ -139,7 +147,7 @@ var _ = Describe("Hypervisor Controller", func() {
139147
Expect(hypervisor.Status.Instances).To(HaveLen(1))
140148
Expect(hypervisor.Status.Instances[0].ID).To(Equal("25e2ea06-f6be-4bac-856d-8c2d0bdbcdee"))
141149

142-
Expect(hypervisor.Status.Conditions).To(HaveLen(3))
150+
Expect(hypervisor.Status.Conditions).To(HaveLen(4))
143151
Expect(hypervisor.Status.Conditions[0].Type).To(Equal("Ready"))
144152
Expect(hypervisor.Status.Conditions[0].Status).To(Equal(metav1.ConditionTrue))
145153
Expect(hypervisor.Status.Conditions[0].Reason).To(Equal("Reconciled"))
@@ -148,9 +156,19 @@ var _ = Describe("Hypervisor Controller", func() {
148156
Expect(hypervisor.Status.Conditions[1].Status).To(Equal(metav1.ConditionTrue))
149157
Expect(hypervisor.Status.Conditions[1].Reason).To(Equal("Connected"))
150158

151-
Expect(hypervisor.Status.Conditions[2].Type).To(Equal("test-unit"))
159+
Expect(hypervisor.Status.Conditions[2].Type).To(Equal("CapabilitiesClientConnection"))
152160
Expect(hypervisor.Status.Conditions[2].Status).To(Equal(metav1.ConditionTrue))
153-
Expect(hypervisor.Status.Conditions[2].Reason).To(Equal("Running"))
161+
Expect(hypervisor.Status.Conditions[2].Reason).To(Equal("CapabilitiesClientGetSucceeded"))
162+
163+
Expect(hypervisor.Status.Conditions[3].Type).To(Equal("test-unit"))
164+
Expect(hypervisor.Status.Conditions[3].Status).To(Equal(metav1.ConditionTrue))
165+
Expect(hypervisor.Status.Conditions[3].Reason).To(Equal("Running"))
166+
167+
Expect(hypervisor.Status.Capabilities.HostCpuArch).To(Equal("x86_64"))
168+
Expect(hypervisor.Status.Capabilities.HostCpus.AsDec().UnscaledBig()).
169+
To(Equal(resource.NewQuantity(4, resource.DecimalSI).AsDec().UnscaledBig()))
170+
Expect(hypervisor.Status.Capabilities.HostMemory.AsDec().UnscaledBig()).
171+
To(Equal(resource.NewQuantity(8192, resource.DecimalSI).AsDec().UnscaledBig()))
154172
})
155173
})
156174
})

internal/emulator/libvirt.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626

2727
"github.com/cobaltcore-dev/kvm-node-agent/api/v1alpha1"
2828
"github.com/cobaltcore-dev/kvm-node-agent/internal/libvirt"
29+
"github.com/cobaltcore-dev/kvm-node-agent/internal/libvirt/capabilities"
2930
)
3031

3132
func NewLibVirtEmulator(ctx context.Context) *libvirt.InterfaceMock {
@@ -55,6 +56,9 @@ func NewLibVirtEmulator(ctx context.Context) *libvirt.InterfaceMock {
5556
log.Info("IsConnectedFunc Func called")
5657
return true
5758
},
59+
GetCapabilitiesFunc: func() (v1alpha1.CapabilitiesStatus, error) {
60+
return capabilities.NewClientEmulator().Get(nil)
61+
},
5862
}
5963
return mockedInterface
6064
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
SPDX-FileCopyrightText: Copyright 2025 SAP SE or an SAP affiliate company and cobaltcore-dev contributors
3+
SPDX-License-Identifier: Apache-2.0
4+
5+
Licensed under the Apache License, LibVirtVersion 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package capabilities
19+
20+
import (
21+
"encoding/xml"
22+
"fmt"
23+
24+
"github.com/cobaltcore-dev/kvm-node-agent/api/v1alpha1"
25+
libvirt "github.com/digitalocean/go-libvirt"
26+
"k8s.io/apimachinery/pkg/api/resource"
27+
"sigs.k8s.io/controller-runtime/pkg/log"
28+
)
29+
30+
// Client that returns the capabilities of the host we are mounted on.
31+
type Client interface {
32+
// Return the capabilities status of the host we are mounted on.
33+
Get(virt *libvirt.Libvirt) (v1alpha1.CapabilitiesStatus, error)
34+
}
35+
36+
// Implementation of the CapabilitiesClient interface.
37+
type client struct{}
38+
39+
// Create a new capabilities client.
40+
func NewClient() Client {
41+
return &client{}
42+
}
43+
44+
// Return the capabilities of the host we are mounted on.
45+
func (m *client) Get(virt *libvirt.Libvirt) (v1alpha1.CapabilitiesStatus, error) {
46+
capabilitiesXMLBytes, err := virt.Capabilities()
47+
if err != nil {
48+
log.Log.Error(err, "failed to get libvirt capabilities")
49+
return v1alpha1.CapabilitiesStatus{}, err
50+
}
51+
var capabilities Capabilities
52+
if err := xml.Unmarshal(capabilitiesXMLBytes, &capabilities); err != nil {
53+
log.Log.Error(err, "failed to unmarshal libvirt capabilities")
54+
return v1alpha1.CapabilitiesStatus{}, err
55+
}
56+
return convert(capabilities)
57+
}
58+
59+
// Emulated capabilities client returning an embedded capabilities xml.
60+
type clientEmulator struct{}
61+
62+
// Create a new emulated capabilities client.
63+
func NewClientEmulator() Client {
64+
return &clientEmulator{}
65+
}
66+
67+
// Get the capabilities of the host we are mounted on.
68+
func (c *clientEmulator) Get(virt *libvirt.Libvirt) (v1alpha1.CapabilitiesStatus, error) {
69+
var capabilities Capabilities
70+
if err := xml.Unmarshal(exampleXML, &capabilities); err != nil {
71+
log.Log.Error(err, "failed to unmarshal example capabilities")
72+
return v1alpha1.CapabilitiesStatus{}, err
73+
}
74+
return convert(capabilities)
75+
}
76+
77+
// Convert the libvirt capabilities to the API format.
78+
func convert(in Capabilities) (out v1alpha1.CapabilitiesStatus, err error) {
79+
out.HostCpuArch = in.Host.CPU.Arch
80+
// Loop over all numa cells to get the total memory + vcpus.
81+
totalMemory := resource.NewQuantity(0, resource.BinarySI)
82+
totalCpus := resource.NewQuantity(0, resource.DecimalSI)
83+
for _, cell := range in.Host.Topology.CellSpec.Cells {
84+
mem, err := cell.Memory.AsQuantity()
85+
if err != nil {
86+
return v1alpha1.CapabilitiesStatus{}, err
87+
}
88+
totalMemory.Add(mem)
89+
cpu := resource.NewQuantity(cell.CPUs.Num, resource.DecimalSI)
90+
if cpu == nil {
91+
return v1alpha1.CapabilitiesStatus{},
92+
fmt.Errorf("invalid CPU count for cell %d", cell.ID)
93+
}
94+
totalCpus.Add(*cpu)
95+
}
96+
out.HostMemory = *totalMemory
97+
out.HostCpus = *totalCpus
98+
return out, nil
99+
}

0 commit comments

Comments
 (0)