Skip to content

Commit 98f320f

Browse files
committed
bootloader: Add rpm-ostree kargs --source=tuned support
On bootc systems with transient /etc, the state file /etc/tuned/bootcmdline is lost after each reboot. This causes TuneD to lose track of which kernel arguments it previously set, leading to kargs stacking up on every reboot cycle. The rpm-ostree --source flag solves this by tracking kargs ownership directly in the BLS config on /boot, which persists across reboots regardless of /etc transience. With --source=tuned, rpm-ostree automatically removes all previous kargs from the "tuned" source and replaces them with the new set, eliminating the need for TuneD to maintain its own state for diffing. This commit adds --source support to the bootloader plugin: 1. Detect --source availability by checking rpm-ostree kargs --help output during plugin init. Uses None-safe string concatenation to handle execute() returning err=None. 2. When --source is available, use "rpm-ostree kargs --source=tuned --append=X --append=Y" for applying kargs and "rpm-ostree kargs --source=tuned" (no --append) for clearing all TuneD-owned kargs on profile removal. 3. When --source is not available (older rpm-ostree), fall back to the existing --delete/--append + bootcmdline state file tracking. No legacy code paths are modified. 4. Continue writing to /etc/tuned/bootcmdline as best-effort for diagnostics and backward compatibility, but do not depend on it for correctness when --source is available. 5. Add unit tests for the bootloader plugin (the first ever in the codebase): 17 tests covering --source detection (including None stderr/stdout edge cases), apply, removal, dispatch logic, error handling, and fallback paths. Assisted-by: OpenCode (Claude Opus 4.6) Signed-off-by: Joseph Marrero Corchado <jmarrero@redhat.com>
1 parent e23e439 commit 98f320f

2 files changed

Lines changed: 339 additions & 0 deletions

File tree

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
import unittest
2+
3+
try:
4+
from unittest.mock import Mock, patch, call
5+
except ImportError:
6+
from mock import Mock, patch, call
7+
8+
import tuned.consts as consts
9+
10+
11+
class BootloaderPluginSourceTestCase(unittest.TestCase):
12+
"""Tests for the rpm-ostree --source=tuned integration in the bootloader plugin.
13+
14+
These tests exercise the --source detection, source-based kargs apply,
15+
source-based kargs removal, and the dispatch logic that falls back to
16+
the legacy code path when --source is not available.
17+
18+
The BootloaderPlugin constructor requires /etc/grub.d/00_tuned to exist,
19+
so we test the methods directly on a minimal mock rather than
20+
instantiating the full plugin.
21+
"""
22+
23+
def _make_plugin_mock(self, has_source=True, rpm_ostree_idle=True):
24+
"""Create a mock that has the real methods under test patched onto it.
25+
26+
This avoids instantiating BootloaderPlugin (which requires GRUB2
27+
template files on disk) while still testing the actual method logic.
28+
"""
29+
from tuned.plugins.plugin_bootloader import BootloaderPlugin
30+
31+
mock_plugin = Mock(spec=BootloaderPlugin)
32+
mock_plugin._rpm_ostree_has_source = has_source
33+
mock_plugin._cmdline_val = ""
34+
mock_plugin._cmd = Mock()
35+
mock_plugin._cmd.add_modify_option_in_file = Mock(return_value=True)
36+
37+
# Make _wait_till_rpm_ostree_idle return the desired value
38+
mock_plugin._wait_till_rpm_ostree_idle = Mock(return_value=rpm_ostree_idle)
39+
40+
# Bind real methods to the mock so we test actual logic
41+
mock_plugin._rpm_ostree_has_source_flag = \
42+
lambda: BootloaderPlugin._rpm_ostree_has_source_flag(mock_plugin)
43+
mock_plugin._rpm_ostree_source_update = \
44+
lambda: BootloaderPlugin._rpm_ostree_source_update(mock_plugin)
45+
mock_plugin._remove_rpm_ostree_source_tuning = \
46+
lambda: BootloaderPlugin._remove_rpm_ostree_source_tuning(mock_plugin)
47+
mock_plugin._rpm_ostree_update = \
48+
lambda: BootloaderPlugin._rpm_ostree_update(mock_plugin)
49+
mock_plugin._remove_rpm_ostree_tuning = \
50+
lambda: BootloaderPlugin._remove_rpm_ostree_tuning(mock_plugin)
51+
mock_plugin._patch_bootcmdline = \
52+
lambda d: BootloaderPlugin._patch_bootcmdline(mock_plugin, d)
53+
54+
return mock_plugin
55+
56+
# ------------------------------------------------------------------
57+
# _rpm_ostree_has_source_flag() detection
58+
# ------------------------------------------------------------------
59+
60+
def test_has_source_flag_present(self):
61+
"""--source flag is detected when present in rpm-ostree kargs --help."""
62+
plugin = self._make_plugin_mock()
63+
help_text = (
64+
"Usage:\n"
65+
" rpm-ostree kargs [OPTION...]\n\n"
66+
" --append=KEY=VALUE Append kernel argument\n"
67+
" --source=NAME Track kargs ownership by source name\n"
68+
" --delete=KEY=VALUE Delete a kernel argument\n"
69+
)
70+
plugin._cmd.execute = Mock(return_value=(0, help_text, ""))
71+
self.assertTrue(plugin._rpm_ostree_has_source_flag())
72+
plugin._cmd.execute.assert_called_once_with(
73+
['rpm-ostree', 'kargs', '--help'], return_err=True)
74+
75+
def test_has_source_flag_absent(self):
76+
"""--source flag is not detected when absent from rpm-ostree kargs --help."""
77+
plugin = self._make_plugin_mock()
78+
help_text = (
79+
"Usage:\n"
80+
" rpm-ostree kargs [OPTION...]\n\n"
81+
" --append=KEY=VALUE Append kernel argument\n"
82+
" --delete=KEY=VALUE Delete a kernel argument\n"
83+
)
84+
plugin._cmd.execute = Mock(return_value=(0, help_text, ""))
85+
self.assertFalse(plugin._rpm_ostree_has_source_flag())
86+
87+
def test_has_source_flag_in_stderr(self):
88+
"""--source flag is detected even if it appears in stderr."""
89+
plugin = self._make_plugin_mock()
90+
plugin._cmd.execute = Mock(return_value=(0, "", "--source=NAME Track kargs"))
91+
self.assertTrue(plugin._rpm_ostree_has_source_flag())
92+
93+
def test_has_source_flag_help_fails(self):
94+
"""If rpm-ostree kargs --help fails, --source is not detected."""
95+
plugin = self._make_plugin_mock()
96+
plugin._cmd.execute = Mock(return_value=(1, "", "command not found"))
97+
self.assertFalse(plugin._rpm_ostree_has_source_flag())
98+
99+
def test_has_source_flag_stderr_none(self):
100+
"""Handle None stderr from successful command without crash."""
101+
plugin = self._make_plugin_mock()
102+
help_text = " --source=NAME Track kargs\n"
103+
plugin._cmd.execute = Mock(return_value=(0, help_text, None))
104+
self.assertTrue(plugin._rpm_ostree_has_source_flag())
105+
106+
def test_has_source_flag_both_none(self):
107+
"""Handle both stdout and stderr being None without crash."""
108+
plugin = self._make_plugin_mock()
109+
plugin._cmd.execute = Mock(return_value=(0, None, None))
110+
self.assertFalse(plugin._rpm_ostree_has_source_flag())
111+
112+
# ------------------------------------------------------------------
113+
# _rpm_ostree_source_update()
114+
# ------------------------------------------------------------------
115+
116+
def test_source_update_basic(self):
117+
"""--source=tuned is called with correct --append flags."""
118+
plugin = self._make_plugin_mock()
119+
plugin._cmdline_val = "nohz=full isolcpus=1-3"
120+
plugin._cmd.execute = Mock(return_value=(0, "", ""))
121+
122+
plugin._rpm_ostree_source_update()
123+
124+
plugin._cmd.execute.assert_called_once_with(
125+
["rpm-ostree", "kargs", "--source=tuned",
126+
"--append=nohz=full", "--append=isolcpus=1-3"],
127+
return_err=True)
128+
# Verify bootcmdline state file is updated
129+
plugin._cmd.add_modify_option_in_file.assert_called_once_with(
130+
consts.BOOT_CMDLINE_FILE,
131+
{consts.BOOT_CMDLINE_TUNED_VAR: "nohz=full isolcpus=1-3"})
132+
133+
def test_source_update_empty_cmdline(self):
134+
"""When cmdline is empty, only --source=tuned is passed (clears kargs)."""
135+
plugin = self._make_plugin_mock()
136+
plugin._cmdline_val = ""
137+
plugin._cmd.execute = Mock(return_value=(0, "", ""))
138+
139+
plugin._rpm_ostree_source_update()
140+
141+
plugin._cmd.execute.assert_called_once_with(
142+
["rpm-ostree", "kargs", "--source=tuned"],
143+
return_err=True)
144+
plugin._cmd.add_modify_option_in_file.assert_called_once_with(
145+
consts.BOOT_CMDLINE_FILE,
146+
{consts.BOOT_CMDLINE_TUNED_VAR: ""})
147+
148+
def test_source_update_rpm_ostree_busy(self):
149+
"""When rpm-ostree is busy, source update does not execute."""
150+
plugin = self._make_plugin_mock(rpm_ostree_idle=False)
151+
plugin._cmdline_val = "nohz=full"
152+
plugin._cmd.execute = Mock()
153+
154+
plugin._rpm_ostree_source_update()
155+
156+
plugin._cmd.execute.assert_not_called()
157+
plugin._cmd.add_modify_option_in_file.assert_not_called()
158+
159+
def test_source_update_command_fails(self):
160+
"""When rpm-ostree kargs --source fails, bootcmdline is not updated."""
161+
plugin = self._make_plugin_mock()
162+
plugin._cmdline_val = "nohz=full"
163+
plugin._cmd.execute = Mock(return_value=(1, "", "error: unknown option"))
164+
165+
plugin._rpm_ostree_source_update()
166+
167+
plugin._cmd.execute.assert_called_once()
168+
plugin._cmd.add_modify_option_in_file.assert_not_called()
169+
170+
# ------------------------------------------------------------------
171+
# _remove_rpm_ostree_source_tuning()
172+
# ------------------------------------------------------------------
173+
174+
def test_remove_source_tuning(self):
175+
"""Removal calls --source=tuned with no --append (clears all)."""
176+
plugin = self._make_plugin_mock()
177+
plugin._cmd.execute = Mock(return_value=(0, "", ""))
178+
179+
plugin._remove_rpm_ostree_source_tuning()
180+
181+
plugin._cmd.execute.assert_called_once_with(
182+
["rpm-ostree", "kargs", "--source=tuned"],
183+
return_err=True)
184+
plugin._cmd.add_modify_option_in_file.assert_called_once_with(
185+
consts.BOOT_CMDLINE_FILE,
186+
{consts.BOOT_CMDLINE_TUNED_VAR: ""})
187+
188+
def test_remove_source_tuning_rpm_ostree_busy(self):
189+
"""When rpm-ostree is busy, source removal does not execute."""
190+
plugin = self._make_plugin_mock(rpm_ostree_idle=False)
191+
plugin._cmd.execute = Mock()
192+
193+
plugin._remove_rpm_ostree_source_tuning()
194+
195+
plugin._cmd.execute.assert_not_called()
196+
# bootcmdline should not be touched either
197+
plugin._cmd.add_modify_option_in_file.assert_not_called()
198+
199+
def test_remove_source_tuning_command_fails(self):
200+
"""When --source removal fails, bootcmdline is still cleared (best-effort)."""
201+
plugin = self._make_plugin_mock()
202+
plugin._cmd.execute = Mock(return_value=(1, "", "error"))
203+
204+
plugin._remove_rpm_ostree_source_tuning()
205+
206+
# bootcmdline should still be cleared even on failure
207+
plugin._cmd.add_modify_option_in_file.assert_called_once_with(
208+
consts.BOOT_CMDLINE_FILE,
209+
{consts.BOOT_CMDLINE_TUNED_VAR: ""})
210+
211+
# ------------------------------------------------------------------
212+
# _rpm_ostree_update() dispatch
213+
# ------------------------------------------------------------------
214+
215+
def test_rpm_ostree_update_dispatches_to_source(self):
216+
"""When --source is available, _rpm_ostree_update uses the source path."""
217+
plugin = self._make_plugin_mock(has_source=True)
218+
plugin._cmdline_val = "nohz=full"
219+
plugin._cmd.execute = Mock(return_value=(0, "", ""))
220+
221+
plugin._rpm_ostree_update()
222+
223+
# Should have called --source=tuned
224+
plugin._cmd.execute.assert_called_once_with(
225+
["rpm-ostree", "kargs", "--source=tuned", "--append=nohz=full"],
226+
return_err=True)
227+
228+
def test_rpm_ostree_update_fallback_when_no_source(self):
229+
"""When --source is not available, _rpm_ostree_update uses the legacy path."""
230+
from tuned.plugins.plugin_bootloader import BootloaderPlugin
231+
232+
plugin = self._make_plugin_mock(has_source=False)
233+
plugin._cmdline_val = "nohz=full"
234+
235+
# Set up legacy path dependencies
236+
plugin._get_appended_rpm_ostree_kargs = Mock(return_value=["old_karg=1"])
237+
plugin._get_rpm_ostree_kargs = Mock(return_value="root=UUID=xxx old_karg=1")
238+
plugin._modify_rpm_ostree_kargs = Mock(return_value=True)
239+
240+
# Bind _rpm_ostree_update with the real method
241+
# but we need to also bind _patch_bootcmdline for the legacy path
242+
plugin._rpm_ostree_update = \
243+
lambda: BootloaderPlugin._rpm_ostree_update(plugin)
244+
245+
plugin._rpm_ostree_update()
246+
247+
# Should NOT have called --source, should use legacy delete/append
248+
plugin._modify_rpm_ostree_kargs.assert_called_once_with(
249+
delete_kargs=["old_karg=1"], append_kargs=["nohz=full"])
250+
251+
# ------------------------------------------------------------------
252+
# _remove_rpm_ostree_tuning() dispatch
253+
# ------------------------------------------------------------------
254+
255+
def test_remove_rpm_ostree_tuning_dispatches_to_source(self):
256+
"""When --source is available, removal uses the source path."""
257+
plugin = self._make_plugin_mock(has_source=True)
258+
plugin._cmd.execute = Mock(return_value=(0, "", ""))
259+
260+
plugin._remove_rpm_ostree_tuning()
261+
262+
plugin._cmd.execute.assert_called_once_with(
263+
["rpm-ostree", "kargs", "--source=tuned"],
264+
return_err=True)
265+
266+
def test_remove_rpm_ostree_tuning_fallback_when_no_source(self):
267+
"""When --source is not available, removal uses the legacy path."""
268+
from tuned.plugins.plugin_bootloader import BootloaderPlugin
269+
270+
plugin = self._make_plugin_mock(has_source=False)
271+
plugin._get_appended_rpm_ostree_kargs = Mock(return_value=["old=1", "old=2"])
272+
plugin._modify_rpm_ostree_kargs = Mock(return_value=True)
273+
274+
plugin._remove_rpm_ostree_tuning = \
275+
lambda: BootloaderPlugin._remove_rpm_ostree_tuning(plugin)
276+
277+
plugin._remove_rpm_ostree_tuning()
278+
279+
plugin._modify_rpm_ostree_kargs.assert_called_once_with(
280+
delete_kargs=["old=1", "old=2"])
281+
plugin._cmd.add_modify_option_in_file.assert_called_once_with(
282+
consts.BOOT_CMDLINE_FILE,
283+
{consts.BOOT_CMDLINE_TUNED_VAR: ""})
284+
285+
286+
if __name__ == '__main__':
287+
unittest.main()

tuned/plugins/plugin_bootloader.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ def _instance_init(self, instance):
194194
self._bls = self._bls_enabled()
195195

196196
self._rpm_ostree = self._rpm_ostree_status() is not None
197+
self._rpm_ostree_has_source = self._rpm_ostree and self._rpm_ostree_has_source_flag()
197198

198199
def _instance_cleanup(self, instance):
199200
pass
@@ -224,6 +225,12 @@ def _rpm_ostree_status(self):
224225
return None
225226
return splited[1]
226227

228+
def _rpm_ostree_has_source_flag(self):
229+
"""Check if the installed rpm-ostree supports --source for kargs."""
230+
(rc, out, err) = self._cmd.execute(
231+
['rpm-ostree', 'kargs', '--help'], return_err=True)
232+
return '--source=' in ((out or '') + (err or ''))
233+
227234
def _wait_till_rpm_ostree_idle(self):
228235
"""Check that rpm-ostree is idle, allowing some waiting time."""
229236
sleep_cycles = 10
@@ -264,6 +271,45 @@ def _modify_rpm_ostree_kargs(self, delete_kargs=[], append_kargs=[]):
264271
return False
265272
return True
266273

274+
def _rpm_ostree_source_update(self):
275+
"""Apply kernel parameter tuning using rpm-ostree --source tracking.
276+
277+
With --source=tuned, rpm-ostree automatically removes all previous
278+
kargs from the 'tuned' source and replaces them with the new set.
279+
This eliminates the need to track state in /etc/tuned/bootcmdline,
280+
which is critical for bootc systems with transient /etc.
281+
"""
282+
if not self._wait_till_rpm_ostree_idle():
283+
log.error("Error modifying rpm-ostree kargs: rpm-ostree is busy")
284+
return
285+
cmd = ["rpm-ostree", "kargs", "--source=tuned"]
286+
for karg in self._cmdline_val.split():
287+
cmd.append("--append=%s" % karg)
288+
(rc, _, err) = self._cmd.execute(cmd, return_err=True)
289+
if rc != 0:
290+
log.error("Error applying rpm-ostree kargs with --source: %s" % err)
291+
return
292+
# Best-effort state file write for diagnostics and backward compat
293+
self._patch_bootcmdline({consts.BOOT_CMDLINE_TUNED_VAR: self._cmdline_val})
294+
295+
def _remove_rpm_ostree_source_tuning(self):
296+
"""Remove kernel parameter tuning using rpm-ostree --source tracking.
297+
298+
Calling --source=tuned with no --append flags clears all kargs
299+
owned by the 'tuned' source.
300+
"""
301+
if not self._wait_till_rpm_ostree_idle():
302+
log.error("Error removing rpm-ostree kargs: rpm-ostree is busy")
303+
return
304+
(rc, _, err) = self._cmd.execute(
305+
["rpm-ostree", "kargs", "--source=tuned"], return_err=True)
306+
if rc != 0:
307+
log.error("Error clearing rpm-ostree kargs source: %s" % err)
308+
# Clear the state file even on rpm-ostree failure to prevent stale
309+
# state from causing incorrect diffs on the next legacy-path apply.
310+
# This matches the behavior of the legacy _remove_rpm_ostree_tuning().
311+
self._patch_bootcmdline({consts.BOOT_CMDLINE_TUNED_VAR: ""})
312+
267313
def _get_effective_options(self, options):
268314
"""Merge provided options with plugin default options and merge all cmdline.* options."""
269315
effective = self._get_config_options().copy()
@@ -336,6 +382,9 @@ def _get_appended_rpm_ostree_kargs(self):
336382

337383
def _remove_rpm_ostree_tuning(self):
338384
"""Remove kernel parameter tuning in a rpm-ostree system."""
385+
if self._rpm_ostree_has_source:
386+
self._remove_rpm_ostree_source_tuning()
387+
return
339388
self._modify_rpm_ostree_kargs(delete_kargs=self._get_appended_rpm_ostree_kargs())
340389
self._patch_bootcmdline({consts.BOOT_CMDLINE_TUNED_VAR: ""})
341390

@@ -448,6 +497,9 @@ def _grub2_cfg_patch(self, d):
448497

449498
def _rpm_ostree_update(self):
450499
"""Apply kernel parameter tuning in a rpm-ostree system."""
500+
if self._rpm_ostree_has_source:
501+
self._rpm_ostree_source_update()
502+
return
451503
appended_kargs = self._get_appended_rpm_ostree_kargs()
452504
profile_kargs = self._cmdline_val.split()
453505
active_kargs = self._get_rpm_ostree_kargs()

0 commit comments

Comments
 (0)