diff --git a/repos/system_upgrade/common/actors/kernelcmdlineconfig/actor.py b/repos/system_upgrade/common/actors/kernelcmdlineconfig/actor.py index 13c471135d..754c036730 100644 --- a/repos/system_upgrade/common/actors/kernelcmdlineconfig/actor.py +++ b/repos/system_upgrade/common/actors/kernelcmdlineconfig/actor.py @@ -1,8 +1,10 @@ import os +from leapp import reporting from leapp.actors import Actor from leapp.exceptions import StopActorExecutionError from leapp.libraries.actor import kernelcmdlineconfig +from leapp.libraries.stdlib import api from leapp.models import FirmwareFacts, InstalledTargetKernelInfo, KernelCmdlineArg, TargetKernelCmdlineArgTasks from leapp.tags import FinalizationPhaseTag, IPUWorkflowTag @@ -29,4 +31,27 @@ def process(self): if ff.firmware == 'bios' and os.path.ismount('/boot/efi'): configs = ['/boot/grub2/grub.cfg', '/boot/efi/EFI/redhat/grub.cfg'] - kernelcmdlineconfig.modify_kernel_args_in_boot_cfg(configs) + + try: + kernelcmdlineconfig.modify_kernel_args_in_boot_cfg(configs) + except kernelcmdlineconfig.ReadOfKernelArgsError as e: + api.current_logger().error(str(e)) + reporting.create_report([ + reporting.Title('Could not retrieve kernel command line arguments: {}'.format(e)), + reporting.Summary( + 'Unable to retrieve the existing kernel command line arguments in order' + ' to set the default value for future installed kernels. After the' + ' system has been rebooted into the new version of RHEL, you should' + ' check what kernel command line options are present in /proc/cmdline' + ' and copy them into /etc/kernel/cmdline before installing any new kernels.' + ), + reporting.Severity(reporting.Severity.MEDIUM), + reporting.Groups([ + reporting.Groups.BOOT, + reporting.Groups.KERNEL, + reporting.Groups.POST, + ]), + reporting.RelatedResource('file', '/etc/kernel/cmdline'), + reporting.RelatedResource('file', '/proc/cmdline'), + ]) + return diff --git a/repos/system_upgrade/common/actors/kernelcmdlineconfig/libraries/kernelcmdlineconfig.py b/repos/system_upgrade/common/actors/kernelcmdlineconfig/libraries/kernelcmdlineconfig.py index f98e8168c4..482d37a129 100644 --- a/repos/system_upgrade/common/actors/kernelcmdlineconfig/libraries/kernelcmdlineconfig.py +++ b/repos/system_upgrade/common/actors/kernelcmdlineconfig/libraries/kernelcmdlineconfig.py @@ -1,9 +1,19 @@ +import re + from leapp.exceptions import StopActorExecutionError from leapp.libraries import stdlib -from leapp.libraries.common.config import architecture +from leapp.libraries.common.config import architecture, version from leapp.libraries.stdlib import api from leapp.models import InstalledTargetKernelInfo, KernelCmdlineArg, TargetKernelCmdlineArgTasks +KERNEL_CMDLINE_FILE = "/etc/kernel/cmdline" + + +class ReadOfKernelArgsError(Exception): + """ + Failed to retrieve the kernel command line arguments + """ + def run_grubby_cmd(cmd): try: @@ -31,12 +41,24 @@ def format_kernelarg_msgs_for_grubby_cmd(kernelarg_msgs): return ' '.join(kernel_args) +def set_default_kernel_args(kernel_args): + if (architecture.matches_architecture(architecture.ARCH_S390X) or + version.matches_target_version(">= 9.0")): + # Put kernel_args into /etc/kernel/cmdline + with open(KERNEL_CMDLINE_FILE, 'w') as f: + f.write(kernel_args) + else: + # Use grub2-editenv to put the kernel args into /boot/grub2/grubenv + stdlib.run(['grub2-editenv', '-', 'set', 'kernelopts={}'.format(kernel_args)]) + + def modify_kernel_args_in_boot_cfg(configs_to_modify_explicitly=None): kernel_info = next(api.consume(InstalledTargetKernelInfo), None) if not kernel_info: return # Collect desired kernelopt modifications + kernelargs_msgs_to_add = list(api.consume(KernelCmdlineArg)) kernelargs_msgs_to_remove = [] for target_kernel_arg_task in api.consume(TargetKernelCmdlineArgTasks): @@ -46,6 +68,8 @@ def modify_kernel_args_in_boot_cfg(configs_to_modify_explicitly=None): if not kernelargs_msgs_to_add and not kernelargs_msgs_to_remove: return # There is no work to do + # Modify the kernel cmdline for the default kernel + grubby_modify_kernelargs_cmd = ['grubby', '--update-kernel={0}'.format(kernel_info.kernel_img_path)] if kernelargs_msgs_to_add: @@ -64,3 +88,23 @@ def modify_kernel_args_in_boot_cfg(configs_to_modify_explicitly=None): run_grubby_cmd(cmd) else: run_grubby_cmd(grubby_modify_kernelargs_cmd) + + # Copy the args for the default kernel to be for all kernels. + + kernel_args = None + cmd = ['grubby', '--info', kernel_info.kernel_img_path] + output = stdlib.run(cmd, split=False) + for record in output['stdout'].splitlines(): + # This could be done with one regex but it's cleaner to parse it as + # structured data. + if record.startswith('args='): + data = record.split("=", 1)[1] + matches = re.match(r'^([\'"]?)(.*)\1$', data) + kernel_args = matches.group(2) + break + else: + raise ReadOfKernelArgsError( + "Failed to retrieve kernel command line to save for future installed kernels." + ) + + set_default_kernel_args(kernel_args) diff --git a/repos/system_upgrade/common/actors/kernelcmdlineconfig/tests/test_kernelcmdlineconfig.py b/repos/system_upgrade/common/actors/kernelcmdlineconfig/tests/test_kernelcmdlineconfig.py index 3f9b2e5e7e..c9fc86ce2e 100644 --- a/repos/system_upgrade/common/actors/kernelcmdlineconfig/tests/test_kernelcmdlineconfig.py +++ b/repos/system_upgrade/common/actors/kernelcmdlineconfig/tests/test_kernelcmdlineconfig.py @@ -1,7 +1,10 @@ +from __future__ import division + from collections import namedtuple import pytest +from leapp.exceptions import StopActorExecutionError from leapp.libraries import stdlib from leapp.libraries.actor import kernelcmdlineconfig from leapp.libraries.common.config import architecture @@ -11,14 +14,43 @@ TARGET_KERNEL_NEVRA = 'kernel-core-1.2.3-4.x86_64.el8.x64_64' +# pylint: disable=E501 +SAMPLE_KERNEL_ARGS = ("ro rootflags=subvol=root" + " resume=/dev/mapper/luks-2c0df999-81ec-4a35-a1f9-b93afee8c6ad" + " rd.luks.uuid=luks-90a6412f-c588-46ca-9118-5aca35943d25" + " rd.luks.uuid=luks-2c0df999-81ec-4a35-a1f9-b93afee8c6ad rhgb quiet" + ) +TEMPLATE_GRUBBY_INFO_OUTPUT = """index=0 +kernel="/boot/vmlinuz-6.5.13-100.fc37.x86_64" +args="{0}" +root="UUID=1aa15850-2685-418d-95a6-f7266a2de83a" +initrd="/boot/initramfs-6.5.13-100.fc37.x86_64.img" +title="Fedora Linux (6.5.13-100.fc37.x86_64) 37 (Thirty Seven)" +id="a3018267cdd8451db7c77bb3e5b1403d-6.5.13-100.fc37.x86_64" +""" # noqa: E501 +SAMPLE_GRUBBY_INFO_OUTPUT = TEMPLATE_GRUBBY_INFO_OUTPUT.format(SAMPLE_KERNEL_ARGS) +# pylint: enable=E501 + class MockedRun(object): - def __init__(self): + def __init__(self, outputs=None): + """ + Mock stdlib.run(). + + If outputs is given, it is a dictionary mapping a cmd to output as stdout. + """ self.commands = [] + self.outputs = outputs or {} def __call__(self, cmd, *args, **kwargs): self.commands.append(cmd) - return {} + return { + "stdout": self.outputs.get(" ".join(cmd), ""), + "stderr": "", + "signal": None, + "exit_code": 0, + "pid": 1234, + } @pytest.mark.parametrize( @@ -50,7 +82,7 @@ def __call__(self, cmd, *args, **kwargs): ), ] ) -def test_kernelcmdline_config_valid_msgs(monkeypatch, msgs, expected_grubby_kernelopt_args): +def test_kernelcmdline_config_valid_msgs(monkeypatch, tmpdir, msgs, expected_grubby_kernelopt_args): kernel_img_path = '/boot/vmlinuz-X' kernel_info = InstalledTargetKernelInfo(pkg_nevra=TARGET_KERNEL_NEVRA, uname_r='', @@ -61,18 +93,28 @@ def test_kernelcmdline_config_valid_msgs(monkeypatch, msgs, expected_grubby_kern grubby_base_cmd = ['grubby', '--update-kernel={}'.format(kernel_img_path)] expected_grubby_cmd = grubby_base_cmd + expected_grubby_kernelopt_args - mocked_run = MockedRun() + mocked_run = MockedRun( + outputs={" ".join(("grubby", "--info", kernel_img_path)): SAMPLE_GRUBBY_INFO_OUTPUT} + ) monkeypatch.setattr(stdlib, 'run', mocked_run) - monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(architecture.ARCH_X86_64, msgs=msgs)) + monkeypatch.setattr(api, 'current_actor', + CurrentActorMocked(architecture.ARCH_X86_64, + dst_ver="8.1", + msgs=msgs) + ) kernelcmdlineconfig.modify_kernel_args_in_boot_cfg() - assert mocked_run.commands and len(mocked_run.commands) == 1 - assert expected_grubby_cmd == mocked_run.commands.pop() + assert mocked_run.commands and len(mocked_run.commands) == 3 + assert expected_grubby_cmd == mocked_run.commands.pop(0) - mocked_run = MockedRun() + mocked_run = MockedRun( + outputs={" ".join(("grubby", "--info", kernel_img_path)): SAMPLE_GRUBBY_INFO_OUTPUT} + ) monkeypatch.setattr(stdlib, 'run', mocked_run) monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(architecture.ARCH_S390X, msgs=msgs)) + monkeypatch.setattr(kernelcmdlineconfig, 'KERNEL_CMDLINE_FILE', str(tmpdir / 'cmdline')) + kernelcmdlineconfig.modify_kernel_args_in_boot_cfg() - assert mocked_run.commands and len(mocked_run.commands) == 2 + assert mocked_run.commands and len(mocked_run.commands) == 3 assert expected_grubby_cmd == mocked_run.commands.pop(0) assert ['/usr/sbin/zipl'] == mocked_run.commands.pop(0) @@ -86,9 +128,17 @@ def test_kernelcmdline_explicit_configs(monkeypatch): initramfs_path='/boot/initramfs-X') msgs = [kernel_info, TargetKernelCmdlineArgTasks(to_remove=[KernelCmdlineArg(key='key1', value='value1')])] - mocked_run = MockedRun() + grubby_cmd_info = ["grubby", "--info", kernel_img_path] + mocked_run = MockedRun( + outputs={" ".join(grubby_cmd_info): SAMPLE_GRUBBY_INFO_OUTPUT} + ) monkeypatch.setattr(stdlib, 'run', mocked_run) - monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(architecture.ARCH_X86_64, msgs=msgs)) + monkeypatch.setattr(api, 'current_actor', + CurrentActorMocked(architecture.ARCH_X86_64, + dst_ver="8.1", + msgs=msgs + ) + ) configs = ['/boot/grub2/grub.cfg', '/boot/efi/EFI/redhat/grub.cfg'] kernelcmdlineconfig.modify_kernel_args_in_boot_cfg(configs_to_modify_explicitly=configs) @@ -97,19 +147,26 @@ def test_kernelcmdline_explicit_configs(monkeypatch): '--remove-args', 'key1=value1'] expected_cmds = [ grubby_cmd_without_config + ['-c', '/boot/grub2/grub.cfg'], - grubby_cmd_without_config + ['-c', '/boot/efi/EFI/redhat/grub.cfg'] + grubby_cmd_without_config + ['-c', '/boot/efi/EFI/redhat/grub.cfg'], + grubby_cmd_info, + ["grub2-editenv", "-", "set", "kernelopts={0}".format(SAMPLE_KERNEL_ARGS)], ] assert mocked_run.commands == expected_cmds def test_kernelcmdline_config_no_args(monkeypatch): + kernel_img_path = '/boot/vmlinuz-X' kernel_info = InstalledTargetKernelInfo(pkg_nevra=TARGET_KERNEL_NEVRA, uname_r='', - kernel_img_path='/boot/vmlinuz-X', + kernel_img_path=kernel_img_path, initramfs_path='/boot/initramfs-X') - mocked_run = MockedRun() + mocked_run = MockedRun( + outputs={" ".join(("grubby", "--info", kernel_img_path)): + TEMPLATE_GRUBBY_INFO_OUTPUT.format("") + } + ) monkeypatch.setattr(stdlib, 'run', mocked_run) monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(architecture.ARCH_S390X, msgs=[kernel_info])) kernelcmdlineconfig.modify_kernel_args_in_boot_cfg() @@ -122,3 +179,32 @@ def test_kernelcmdline_config_no_version(monkeypatch): monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(architecture.ARCH_S390X)) kernelcmdlineconfig.modify_kernel_args_in_boot_cfg() assert not mocked_run.commands + + +def test_kernelcmdline_config_malformed_args(monkeypatch): + kernel_img_path = '/boot/vmlinuz-X' + kernel_info = InstalledTargetKernelInfo(pkg_nevra=TARGET_KERNEL_NEVRA, + uname_r='', + kernel_img_path=kernel_img_path, + initramfs_path='/boot/initramfs-X') + + # For this test, we need to check we get the proper error if grubby --info + # doesn't output any args information at all. + grubby_info_output = "\n".join(line for line in SAMPLE_GRUBBY_INFO_OUTPUT.splitlines() + if not line.startswith("args=")) + mocked_run = MockedRun( + outputs={" ".join(("grubby", "--info", kernel_img_path)): grubby_info_output, + } + ) + msgs = [kernel_info, + TargetKernelCmdlineArgTasks(to_remove=[ + KernelCmdlineArg(key='key1', value='value1')]) + ] + monkeypatch.setattr(stdlib, 'run', mocked_run) + monkeypatch.setattr(api, 'current_actor', + CurrentActorMocked(architecture.ARCH_S390X, msgs=msgs)) + + with pytest.raises(kernelcmdlineconfig.ReadOfKernelArgsError, + match="Failed to retrieve kernel command line to save for future" + " installed kernels."): + kernelcmdlineconfig.modify_kernel_args_in_boot_cfg()