Skip to content

Commit 1631fd4

Browse files
committed
Add kernel parameter reporting to hypervisor status
This enables the operator to report kernel boot parameters in the Hypervisor status, allowing users to verify kernel configuration across the fleet.
1 parent 81fc9df commit 1631fd4

7 files changed

Lines changed: 328 additions & 7 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,10 @@ spec:
459459
hardwareVendor:
460460
description: HardwareVendor
461461
type: string
462+
kernelCommandLine:
463+
description: KernelCommandLine contains the raw kernel boot
464+
parameters from /proc/cmdline.
465+
type: string
462466
kernelName:
463467
description: KernelName
464468
type: string

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ require (
1414
github.com/onsi/ginkgo/v2 v2.28.1
1515
github.com/onsi/gomega v1.39.1
1616
github.com/sapcc/go-api-declarations v1.19.0
17+
github.com/stretchr/testify v1.11.1
1718
k8s.io/api v0.35.0
1819
k8s.io/apimachinery v0.35.0
1920
k8s.io/client-go v0.35.0
@@ -104,6 +105,7 @@ require (
104105
google.golang.org/protobuf v1.36.11 // indirect
105106
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
106107
gopkg.in/inf.v0 v0.9.1 // indirect
108+
gopkg.in/yaml.v3 v3.0.1 // indirect
107109
k8s.io/apiextensions-apiserver v0.35.0 // indirect
108110
k8s.io/apiserver v0.35.0 // indirect
109111
k8s.io/component-base v0.35.0 // indirect

internal/controller/hypervisor_controller.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838

3939
"github.com/cobaltcore-dev/kvm-node-agent/internal/certificates"
4040
"github.com/cobaltcore-dev/kvm-node-agent/internal/evacuation"
41+
"github.com/cobaltcore-dev/kvm-node-agent/internal/kernel"
4142
"github.com/cobaltcore-dev/kvm-node-agent/internal/libvirt"
4243
"github.com/cobaltcore-dev/kvm-node-agent/internal/sys"
4344
"github.com/cobaltcore-dev/kvm-node-agent/internal/systemd"
@@ -46,11 +47,13 @@ import (
4647
// HypervisorReconciler reconciles a Hypervisor object
4748
type HypervisorReconciler struct {
4849
client.Client
49-
Scheme *runtime.Scheme
50-
Systemd systemd.Interface
51-
Libvirt libvirt.Interface
50+
Scheme *runtime.Scheme
51+
Systemd systemd.Interface
52+
Libvirt libvirt.Interface
53+
KernelReader kernel.Interface
5254

5355
osDescriptor *systemd.Descriptor
56+
kernelParameters *kernel.Parameters
5457
evacuateOnReboot bool
5558

5659
// Channel that can be used to trigger reconcile events.
@@ -149,6 +152,11 @@ func (r *HypervisorReconciler) Reconcile(ctx context.Context, req ctrl.Request)
149152
hypervisor.Status.OperatingSystem.FirmwareDate = metav1.NewTime(time.UnixMicro(r.osDescriptor.FirmwareDate))
150153
}
151154

155+
if r.kernelParameters != nil &&
156+
hypervisor.Status.OperatingSystem.KernelCommandLine == "" {
157+
hypervisor.Status.OperatingSystem.KernelCommandLine = r.kernelParameters.CommandLine
158+
}
159+
152160
if hypervisor.Spec.EvacuateOnReboot != r.evacuateOnReboot {
153161
if hypervisor.Spec.EvacuateOnReboot {
154162
e := &evacuation.EvictionController{Client: r.Client}
@@ -360,6 +368,13 @@ func (r *HypervisorReconciler) SetupWithManager(mgr ctrl.Manager) error {
360368
return fmt.Errorf("unable to get Systemd hostname describe(): %w", err)
361369
}
362370

371+
if r.KernelReader == nil {
372+
r.KernelReader = kernel.NewSystemReader()
373+
}
374+
if r.kernelParameters, err = r.KernelReader.ReadParameters(); err != nil {
375+
return fmt.Errorf("unable to read kernel parameters: %w", err)
376+
}
377+
363378
// Prepare an event channel that will trigger a reconcile event.
364379
r.reconcileCh = make(chan event.GenericEvent)
365380
src := source.Channel(r.reconcileCh, &handler.EnqueueRequestForObject{})

internal/controller/hypervisor_controller_test.go

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"sigs.k8s.io/controller-runtime/pkg/event"
3636
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3737

38+
"github.com/cobaltcore-dev/kvm-node-agent/internal/kernel"
3839
"github.com/cobaltcore-dev/kvm-node-agent/internal/libvirt"
3940
"github.com/cobaltcore-dev/kvm-node-agent/internal/sys"
4041
"github.com/cobaltcore-dev/kvm-node-agent/internal/systemd"
@@ -127,13 +128,22 @@ var _ = Describe("Hypervisor Controller", func() {
127128
})
128129

129130
Context("When testing SetupWithManager method", func() {
130-
It("should successfully setup controller with manager", func() {
131+
It("should successfully setup controller with manager and read kernel parameters", func() {
131132
// Create a test manager
132133
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
133134
Scheme: k8sClient.Scheme(),
134135
})
135136
Expect(err).NotTo(HaveOccurred())
136137

138+
// Use mock kernel reader
139+
mockKernelReader := &kernel.InterfaceMock{
140+
ReadParametersFunc: func() (*kernel.Parameters, error) {
141+
return &kernel.Parameters{
142+
CommandLine: "quiet splash console=ttyS0 intel_iommu=on",
143+
}, nil
144+
},
145+
}
146+
137147
controllerReconciler := &HypervisorReconciler{
138148
Client: k8sClient,
139149
Scheme: k8sClient.Scheme(),
@@ -156,13 +166,21 @@ var _ = Describe("Hypervisor Controller", func() {
156166
}, nil
157167
},
158168
},
169+
KernelReader: mockKernelReader,
159170
}
160171

161172
err = controllerReconciler.SetupWithManager(mgr)
162173
Expect(err).NotTo(HaveOccurred())
163174
Expect(controllerReconciler.reconcileCh).NotTo(BeNil())
164175
Expect(controllerReconciler.osDescriptor).NotTo(BeNil())
165176
Expect(controllerReconciler.osDescriptor.OperatingSystemReleaseData).To(HaveLen(2))
177+
178+
// Verify that kernel reader was called and parameters were stored
179+
Expect(mockKernelReader.ReadParametersCalls()).To(HaveLen(1))
180+
Expect(controllerReconciler.kernelParameters).NotTo(BeNil())
181+
Expect(
182+
controllerReconciler.kernelParameters.CommandLine,
183+
).To(Equal("quiet splash console=ttyS0 intel_iommu=on"))
166184
})
167185

168186
It("should fail when systemd Describe returns error", func() {
@@ -172,9 +190,16 @@ var _ = Describe("Hypervisor Controller", func() {
172190
})
173191
Expect(err).NotTo(HaveOccurred())
174192

193+
mockKernelReader := &kernel.InterfaceMock{
194+
ReadParametersFunc: func() (*kernel.Parameters, error) {
195+
return &kernel.Parameters{CommandLine: "quiet splash"}, nil
196+
},
197+
}
198+
175199
controllerReconciler := &HypervisorReconciler{
176-
Client: k8sClient,
177-
Scheme: k8sClient.Scheme(),
200+
Client: k8sClient,
201+
Scheme: k8sClient.Scheme(),
202+
KernelReader: mockKernelReader,
178203
Systemd: &systemd.InterfaceMock{
179204
DescribeFunc: func(ctx context.Context) (*systemd.Descriptor, error) {
180205
return nil, errors.New("systemd describe failed")
@@ -187,6 +212,39 @@ var _ = Describe("Hypervisor Controller", func() {
187212
Expect(err.Error()).To(ContainSubstring("unable to get Systemd hostname describe()"))
188213
Expect(err.Error()).To(ContainSubstring("systemd describe failed"))
189214
})
215+
216+
It("should fail when kernel parameters cannot be read", func() {
217+
// Create a test manager
218+
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
219+
Scheme: k8sClient.Scheme(),
220+
})
221+
Expect(err).NotTo(HaveOccurred())
222+
223+
mockKernelReader := &kernel.InterfaceMock{
224+
ReadParametersFunc: func() (*kernel.Parameters, error) {
225+
return nil, errors.New("failed to read /proc/cmdline")
226+
},
227+
}
228+
229+
controllerReconciler := &HypervisorReconciler{
230+
Client: k8sClient,
231+
Scheme: k8sClient.Scheme(),
232+
KernelReader: mockKernelReader,
233+
Systemd: &systemd.InterfaceMock{
234+
DescribeFunc: func(ctx context.Context) (*systemd.Descriptor, error) {
235+
return &systemd.Descriptor{
236+
KernelVersion: "6.1.0",
237+
}, nil
238+
},
239+
},
240+
}
241+
242+
err = controllerReconciler.SetupWithManager(mgr)
243+
Expect(err).To(HaveOccurred())
244+
Expect(err.Error()).To(ContainSubstring("unable to read kernel parameters"))
245+
Expect(err.Error()).To(ContainSubstring("failed to read /proc/cmdline"))
246+
Expect(mockKernelReader.ReadParametersCalls()).To(HaveLen(1))
247+
})
190248
})
191249

192250
Context("When reconciling a resource", func() {
@@ -227,8 +285,9 @@ var _ = Describe("Hypervisor Controller", func() {
227285
By("Cleanup the specific resource instance Hypervisor")
228286
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
229287
})
230-
It("should successfully reconcile the resource", func() {
288+
It("should successfully reconcile the resource with kernel parameters", func() {
231289
By("Reconciling the created resource")
290+
232291
controllerReconciler := &HypervisorReconciler{
233292
Client: k8sClient,
234293
Scheme: k8sClient.Scheme(),
@@ -289,6 +348,9 @@ var _ = Describe("Hypervisor Controller", func() {
289348
"VARIANT_ID=metal-sci_usi-amd64",
290349
},
291350
},
351+
kernelParameters: &kernel.Parameters{
352+
CommandLine: "quiet splash console=ttyS0 intel_iommu=on",
353+
},
292354
}
293355

294356
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
@@ -321,6 +383,9 @@ var _ = Describe("Hypervisor Controller", func() {
321383
Expect(hypervisor.Status.OperatingSystem.GardenLinuxCommitID).To(Equal("abcdef1234567890"))
322384
Expect(hypervisor.Status.OperatingSystem.GardenLinuxFeatures).To(Equal([]string{"_rescue", "log", "sap"}))
323385
Expect(hypervisor.Status.OperatingSystem.VariantID).To(Equal("metal-sci_usi-amd64"))
386+
Expect(
387+
hypervisor.Status.OperatingSystem.KernelCommandLine,
388+
).To(Equal("quiet splash console=ttyS0 intel_iommu=on"))
324389
})
325390
})
326391
})

internal/kernel/interface_mock.go

Lines changed: 67 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/kernel/parameters.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
SPDX-FileCopyrightText: Copyright 2024 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, Version 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 kernel provides functions to read kernel parameters from the system.
19+
package kernel
20+
21+
import (
22+
"os"
23+
"strings"
24+
)
25+
26+
// DefaultCmdlinePath is the default path to the kernel command line.
27+
const DefaultCmdlinePath = "/proc/cmdline"
28+
29+
// Parameters holds kernel boot parameters.
30+
type Parameters struct {
31+
// CommandLine contains the raw kernel boot parameters from /proc/cmdline.
32+
CommandLine string
33+
}
34+
35+
// Interface provides an interface for reading kernel parameters.
36+
type Interface interface {
37+
// ReadParameters reads and returns kernel parameters from the system.
38+
ReadParameters() (*Parameters, error)
39+
}
40+
41+
// SystemReader reads kernel parameters from the actual system files.
42+
type SystemReader struct {
43+
cmdlinePath string
44+
}
45+
46+
// NewSystemReader creates a new SystemReader with the default cmdline path.
47+
func NewSystemReader() *SystemReader {
48+
return &SystemReader{
49+
cmdlinePath: DefaultCmdlinePath,
50+
}
51+
}
52+
53+
// NewSystemReaderWithPath creates a new SystemReader with a custom cmdline path.
54+
// This is useful for testing.
55+
func NewSystemReaderWithPath(cmdlinePath string) *SystemReader {
56+
return &SystemReader{
57+
cmdlinePath: cmdlinePath,
58+
}
59+
}
60+
61+
// ReadParameters reads kernel parameters from /proc/cmdline and returns them
62+
// as a Parameters struct with the raw command line content.
63+
func (r *SystemReader) ReadParameters() (*Parameters, error) {
64+
data, err := os.ReadFile(r.cmdlinePath)
65+
if err != nil {
66+
return nil, err
67+
}
68+
69+
cmdline := strings.TrimSpace(string(data))
70+
71+
return &Parameters{CommandLine: cmdline}, nil
72+
}

0 commit comments

Comments
 (0)