From c2c96affa7b20c82969419ce49b65cbf646a0c32 Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Fri, 18 Oct 2024 12:43:19 +0200
Subject: [PATCH 11/40] kernelcmdlineconfig: Use args from first entry when
 multiple entries are listed

Instead of erroring out when grubby lists multiple entries for the
default kernel, always use the `args=` and `root=` from the first one and create
a post-upgrade report. The report instruct user to ensure those are the
correct ones or to correct them.

This can happen, for example, if MAKEDEBUG=yes is set in
/etc/sysconfing/kernel.

Jira: RHEL-46911
---
 .../libraries/kernelcmdlineconfig.py          | 79 ++++++++++++++++---
 .../tests/test_kernelcmdlineconfig.py         | 48 ++++++++++-
 2 files changed, 116 insertions(+), 11 deletions(-)

diff --git a/repos/system_upgrade/common/actors/kernelcmdlineconfig/libraries/kernelcmdlineconfig.py b/repos/system_upgrade/common/actors/kernelcmdlineconfig/libraries/kernelcmdlineconfig.py
index 6b261c3b..19c50f3c 100644
--- a/repos/system_upgrade/common/actors/kernelcmdlineconfig/libraries/kernelcmdlineconfig.py
+++ b/repos/system_upgrade/common/actors/kernelcmdlineconfig/libraries/kernelcmdlineconfig.py
@@ -109,10 +109,55 @@ def _extract_grubby_value(record):
     return matches.group(2)
 
 
+def report_multple_entries_for_default_kernel():
+    if use_cmdline_file():
+        report_hint = (
+            'After the system has been rebooted into the new version of RHEL,'
+            ' check that configured default kernel cmdline arguments in /etc/kernel/cmdline '
+            ' are correct. In case that different arguments are expected, update the file as needed.'
+        )
+    else:
+        report_hint = (
+            'After the system has been rebooted into the new version of RHEL,'
+            ' check that configured default kernel cmdline arguments are set as expected, using'
+            ' the `grub2-editenv list` command. '
+            ' If different default arguments are expected, update them using grub2-editenv.\n'
+            ' For example, consider that current booted kernel has correct kernel cmdline'
+            ' arguments and /proc/cmdline contains:\n\n'
+            '    BOOT_IMAGE=(hd0,msdos1)/vmlinuz-4.18.0-425.3.1.el8.x86_64'
+            ' root=/dev/mapper/rhel_ibm--root ro console=tty0'
+            ' console=ttyS0,115200 rd_NO_PLYMOUTH\n\n'
+            ' then run the following grub2-editenv command:\n\n'
+            '    # grub2-editenv - set "kernelopts=root=/dev/mapper/rhel_ibm--root'
+            ' ro console=tty0 console=ttyS0,115200 rd_NO_PLYMOUTH"'
+        )
+
+    reporting.create_report([
+        reporting.Title('Ensure that expected default kernel cmdline arguments are set'),
+        reporting.Summary(
+            'During the upgrade we needed to modify the kernel command line arguments.'
+            ' However, multiple bootloader entries with different arguments were found for the default'
+            ' kernel (perhaps MAKEDEBUG=yes is set in /etc/sysconfig/kernel).'
+            ' Leapp used the arguments from the first found entry of the target kernel'
+            ' and set it as the new default kernel cmdline arguments for kernels installed in the future.'
+        ),
+        reporting.Remediation(hint=report_hint),
+        reporting.Severity(reporting.Severity.HIGH),
+        reporting.Groups([
+            reporting.Groups.BOOT,
+            reporting.Groups.KERNEL,
+            reporting.Groups.POST,
+        ]),
+        reporting.RelatedResource('file', '/etc/kernel/cmdline'),
+    ])
+
+
 def retrieve_args_for_default_kernel(kernel_info):
     # Copy the args for the default kernel to all kernels.
     kernel_args = None
     kernel_root = None
+    detected_multiple_entries = False
+
     cmd = ['grubby', '--info', kernel_info.kernel_img_path]
     output = stdlib.run(cmd, split=False)
     for record in output['stdout'].splitlines():
@@ -122,19 +167,30 @@ def retrieve_args_for_default_kernel(kernel_info):
             temp_kernel_args = _extract_grubby_value(record)
 
             if kernel_args:
-                api.current_logger().warning('Grubby output is malformed:'
-                                             ' `args=` is listed more than once.')
                 if kernel_args != temp_kernel_args:
-                    raise ReadOfKernelArgsError('Grubby listed `args=` multiple'
-                                                ' times with different values.')
-            kernel_args = _extract_grubby_value(record)
+                    api.current_logger().warning(
+                        'Grubby output listed `args=` multiple times with different values,'
+                        ' continuing with the first result'
+                    )
+                    detected_multiple_entries = True
+                else:
+                    api.current_logger().warning('Grubby output listed `args=` more than once')
+            else:
+                kernel_args = temp_kernel_args
         elif record.startswith('root='):
-            api.current_logger().warning('Grubby output is malformed:'
-                                         ' `root=` is listed more than once.')
+            temp_kernel_root = _extract_grubby_value(record)
+
             if kernel_root:
-                raise ReadOfKernelArgsError('Grubby listed `root=` multiple'
-                                            ' times with different values')
-            kernel_root = _extract_grubby_value(record)
+                if kernel_root != temp_kernel_root:
+                    api.current_logger().warning(
+                        'Grubby output listed `root=` multiple times with different values,'
+                        ' continuing with the first result'
+                    )
+                    detected_multiple_entries = True
+                else:
+                    api.current_logger().warning('Grubby output listed `root=` more than once')
+            else:
+                kernel_root = temp_kernel_root
 
     if not kernel_args or not kernel_root:
         raise ReadOfKernelArgsError(
@@ -142,6 +198,9 @@ def retrieve_args_for_default_kernel(kernel_info):
             ' kernels: root={}, args={}'.format(kernel_root, kernel_args)
         )
 
+    if detected_multiple_entries:
+        report_multple_entries_for_default_kernel()
+
     return kernel_root, 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 ffe4b046..e5759a7b 100644
--- a/repos/system_upgrade/common/actors/kernelcmdlineconfig/tests/test_kernelcmdlineconfig.py
+++ b/repos/system_upgrade/common/actors/kernelcmdlineconfig/tests/test_kernelcmdlineconfig.py
@@ -4,11 +4,12 @@ from collections import namedtuple
 
 import pytest
 
+from leapp import reporting
 from leapp.exceptions import StopActorExecutionError
 from leapp.libraries import stdlib
 from leapp.libraries.actor import kernelcmdlineconfig
 from leapp.libraries.common.config import architecture
-from leapp.libraries.common.testutils import CurrentActorMocked
+from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked
 from leapp.libraries.stdlib import api
 from leapp.models import InstalledTargetKernelInfo, KernelCmdlineArg, TargetKernelCmdlineArgTasks
 
@@ -183,6 +184,51 @@ def test_kernelcmdline_config_no_version(monkeypatch):
     assert not mocked_run.commands
 
 
+SECOND_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'
+)
+SECOND_KERNEL_ROOT = 'UUID=1aa15850-2685-418d-95a6-f7266a2de83b'
+
+
+@pytest.mark.parametrize(
+    'second_grubby_output',
+    (
+        TEMPLATE_GRUBBY_INFO_OUTPUT.format(SECOND_KERNEL_ARGS, SECOND_KERNEL_ROOT),
+        TEMPLATE_GRUBBY_INFO_OUTPUT.format(SAMPLE_KERNEL_ARGS, SECOND_KERNEL_ROOT),
+        TEMPLATE_GRUBBY_INFO_OUTPUT.format(SECOND_KERNEL_ARGS, SAMPLE_KERNEL_ROOT),
+    )
+)
+def test_kernelcmdline_config_mutiple_args(monkeypatch, second_grubby_output):
+    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 report if grubby --info
+    # outputs multiple different `root=` or `args=`
+    # and that the first ones are used
+    grubby_info_output = "\n".join((SAMPLE_GRUBBY_INFO_OUTPUT, second_grubby_output))
+
+    mocked_run = MockedRun(
+        outputs={" ".join(("grubby", "--info", kernel_img_path)): grubby_info_output,
+                 }
+    )
+    monkeypatch.setattr(stdlib, 'run', mocked_run)
+    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
+    monkeypatch.setattr(reporting, "create_report", create_report_mocked())
+
+    root, args = kernelcmdlineconfig.retrieve_args_for_default_kernel(kernel_info)
+    assert root == SAMPLE_KERNEL_ROOT
+    assert args == SAMPLE_KERNEL_ARGS
+    assert reporting.create_report.called == 1
+    expected_title = 'Ensure that expected default kernel cmdline arguments are set'
+    assert expected_title in reporting.create_report.report_fields['title']
+
+
 def test_kernelcmdline_config_malformed_args(monkeypatch):
     kernel_img_path = '/boot/vmlinuz-X'
     kernel_info = InstalledTargetKernelInfo(pkg_nevra=TARGET_KERNEL_NEVRA,
-- 
2.47.0