Skip to content

Commit 5375e27

Browse files
Add capabilities self-discovery
1 parent b267332 commit 5375e27

File tree

10 files changed

+1566
-6
lines changed

10 files changed

+1566
-6
lines changed

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"`

cmd/main.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/cobaltcode-dev/kvm-node-agent/internal/certificates"
3030
"github.com/cobaltcode-dev/kvm-node-agent/internal/emulator"
3131
"github.com/cobaltcode-dev/kvm-node-agent/internal/libvirt"
32+
"github.com/cobaltcode-dev/kvm-node-agent/internal/libvirt/capabilities"
3233
"github.com/cobaltcode-dev/kvm-node-agent/internal/sys"
3334
"github.com/cobaltcode-dev/kvm-node-agent/internal/systemd"
3435

@@ -176,10 +177,12 @@ func main() {
176177

177178
var sysd systemd.Interface
178179
var libv libvirt.Interface
180+
var capabilitiesClient capabilities.Client
179181
if os.Getenv("EMULATE") != "" {
180182
ctx := logger.IntoContext(context.Background(), setupLog)
181183
libv = emulator.NewLibVirtEmulator(ctx)
182184
sysd = emulator.NewSystemdEmulator(ctx)
185+
capabilitiesClient = capabilities.NewClientEmulator()
183186
} else {
184187
var err error
185188
ctx := logger.IntoContext(context.Background(), setupLog)
@@ -189,13 +192,15 @@ func main() {
189192
setupLog.Error(err, "unable to create systemd instance")
190193
os.Exit(1)
191194
}
195+
capabilitiesClient = capabilities.NewClient()
192196
}
193197

194198
if err = (&controller.HypervisorReconciler{
195-
Client: mgr.GetClient(),
196-
Scheme: mgr.GetScheme(),
197-
Systemd: sysd,
198-
Libvirt: libv,
199+
Client: mgr.GetClient(),
200+
Scheme: mgr.GetScheme(),
201+
Systemd: sysd,
202+
Libvirt: libv,
203+
CapabilitiesClient: capabilitiesClient,
199204
}).SetupWithManager(mgr); err != nil {
200205
setupLog.Error(err, "unable to create controller", "controller", "Hypervisor")
201206
os.Exit(1)

internal/controller/hypervisor_controller.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"github.com/cobaltcode-dev/kvm-node-agent/internal/certificates"
3737
"github.com/cobaltcode-dev/kvm-node-agent/internal/evacuation"
3838
"github.com/cobaltcode-dev/kvm-node-agent/internal/libvirt"
39+
"github.com/cobaltcode-dev/kvm-node-agent/internal/libvirt/capabilities"
3940
"github.com/cobaltcode-dev/kvm-node-agent/internal/sys"
4041
"github.com/cobaltcode-dev/kvm-node-agent/internal/systemd"
4142
)
@@ -47,13 +48,17 @@ type HypervisorReconciler struct {
4748
Systemd systemd.Interface
4849
Libvirt libvirt.Interface
4950

51+
// Client that connects to libvirt and fetches capabilities of the hypervisor.
52+
CapabilitiesClient capabilities.Client
53+
5054
osDescriptor *systemd.Descriptor
5155
evacuateOnReboot bool
5256
}
5357

5458
const (
55-
OSUpdateType = "OperatingSystemUpdate"
56-
LibVirtType = "LibVirtConnection"
59+
OSUpdateType = "OperatingSystemUpdate"
60+
LibVirtType = "LibVirtConnection"
61+
CapabilitiesClientType = "CapabilitiesClientConnection"
5762
)
5863

5964
// +kubebuilder:rbac:groups=kvm.cloud.sap,resources=hypervisors,verbs=get;list;watch;create;update;patch;delete
@@ -133,6 +138,19 @@ func (r *HypervisorReconciler) Reconcile(ctx context.Context, req ctrl.Request)
133138
hypervisor.Status.Instances, _ = r.Libvirt.GetInstances()
134139
}
135140

141+
// Update capabilities status.
142+
var err error
143+
hypervisor.Status.Capabilities, err = r.CapabilitiesClient.Get()
144+
if err != nil {
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+
}
153+
136154
// ====================================================================================================
137155
// Systemd
138156
// ====================================================================================================

internal/controller/hypervisor_controller_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232

3333
kvmv1alpha1 "github.com/cobaltcode-dev/kvm-node-agent/api/v1alpha1"
3434
"github.com/cobaltcode-dev/kvm-node-agent/internal/libvirt"
35+
"github.com/cobaltcode-dev/kvm-node-agent/internal/libvirt/capabilities"
3536
"github.com/cobaltcode-dev/kvm-node-agent/internal/sys"
3637
"github.com/cobaltcode-dev/kvm-node-agent/internal/systemd"
3738
)
@@ -127,6 +128,7 @@ var _ = Describe("Hypervisor Controller", func() {
127128
return nil, nil
128129
},
129130
},
131+
CapabilitiesClient: capabilities.NewClientEmulator(),
130132
}
131133

132134
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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+
"os"
24+
25+
"github.com/cobaltcode-dev/kvm-node-agent/api/v1alpha1"
26+
libvirt "github.com/digitalocean/go-libvirt"
27+
"github.com/digitalocean/go-libvirt/socket/dialers"
28+
"k8s.io/apimachinery/pkg/api/resource"
29+
"sigs.k8s.io/controller-runtime/pkg/log"
30+
)
31+
32+
// Client that returns the capabilities of the host we are mounted on.
33+
type Client interface {
34+
// Return the capabilities status of the host we are mounted on.
35+
Get() (v1alpha1.CapabilitiesStatus, error)
36+
}
37+
38+
// Implementation of the CapabilitiesClient interface.
39+
type client struct {
40+
// Libvirt instance to connect to.
41+
virt *libvirt.Libvirt
42+
}
43+
44+
// Create a new capabilities client using the provided LIBVIRT_SOCKET env variable.
45+
func NewClient() Client {
46+
socketPath := os.Getenv("LIBVIRT_SOCKET")
47+
if socketPath == "" {
48+
socketPath = "/run/libvirt/libvirt-sock"
49+
}
50+
log.Log.Info("capabilities client uses libvirt socket", "socket", socketPath)
51+
dialer := dialers.NewLocal(dialers.WithSocket(socketPath))
52+
virt := libvirt.NewWithDialer(dialer)
53+
return &client{virt: virt}
54+
}
55+
56+
// Return the capabilities of the host we are mounted on.
57+
func (m *client) Get() (v1alpha1.CapabilitiesStatus, error) {
58+
if !m.virt.IsConnected() {
59+
if err := m.virt.Connect(); err != nil {
60+
log.Log.Error(err, "failed to connect to libvirt")
61+
return v1alpha1.CapabilitiesStatus{}, err
62+
}
63+
}
64+
capabilitiesXMLBytes, err := m.virt.Capabilities()
65+
if err != nil {
66+
log.Log.Error(err, "failed to get libvirt capabilities")
67+
return v1alpha1.CapabilitiesStatus{}, err
68+
}
69+
var capabilities Capabilities
70+
if err := xml.Unmarshal(capabilitiesXMLBytes, &capabilities); err != nil {
71+
log.Log.Error(err, "failed to unmarshal libvirt capabilities")
72+
return v1alpha1.CapabilitiesStatus{}, err
73+
}
74+
return convert(capabilities)
75+
}
76+
77+
// Emulated capabilities client returning an embedded capabilities xml.
78+
type clientEmulator struct{}
79+
80+
// Create a new emulated capabilities client.
81+
func NewClientEmulator() Client {
82+
return &clientEmulator{}
83+
}
84+
85+
// Get the capabilities of the host we are mounted on.
86+
func (c *clientEmulator) Get() (v1alpha1.CapabilitiesStatus, error) {
87+
var capabilities Capabilities
88+
if err := xml.Unmarshal(exampleXML, &capabilities); err != nil {
89+
log.Log.Error(err, "failed to unmarshal example capabilities")
90+
return v1alpha1.CapabilitiesStatus{}, err
91+
}
92+
return convert(capabilities)
93+
}
94+
95+
// Convert the libvirt capabilities to the API format.
96+
func convert(in Capabilities) (out v1alpha1.CapabilitiesStatus, err error) {
97+
out.HostCpuArch = in.Host.CPU.Arch
98+
// Loop over all numa cells to get the total memory + vcpus.
99+
totalMemory := resource.NewQuantity(0, resource.BinarySI)
100+
totalCpus := resource.NewQuantity(0, resource.DecimalSI)
101+
for _, cell := range in.Host.Topology.CellSpec.Cells {
102+
mem, err := cell.Memory.AsQuantity()
103+
if err != nil {
104+
return v1alpha1.CapabilitiesStatus{}, err
105+
}
106+
totalMemory.Add(mem)
107+
cpu := resource.NewQuantity(cell.CPUs.Num, resource.DecimalSI)
108+
if cpu == nil {
109+
return v1alpha1.CapabilitiesStatus{},
110+
fmt.Errorf("invalid CPU count for cell %d", cell.ID)
111+
}
112+
totalCpus.Add(*cpu)
113+
}
114+
out.HostMemory = *totalMemory
115+
out.HostCpus = *totalCpus
116+
return out, nil
117+
}

0 commit comments

Comments
 (0)