From 40d0fd61140963b162ec13be4769860236a386ed Mon Sep 17 00:00:00 2001 From: Daniel Diblik <8378124+danmyway@users.noreply.github.com> Date: Wed, 21 Aug 2024 18:46:48 +0200 Subject: [PATCH] [RHELC-1672, RHELC-1707, RHELC-1708] Detect a newer version of RHEL kernel in the main transaction (#1323) * Detect a newer version of RHEL kernel in main transaction * detect if the installed RHEL kernel either replaces all original kernel packages or if a newer version is installed Signed-off-by: Daniel Diblik * Address review comments Signed-off-by: Daniel Diblik * Fix unit tests Signed-off-by: Daniel Diblik * Fix incomplete pkghandler.get_highest_package_version * the function was not complete and the evaluation was wrong (e.g. "9" > "10") Signed-off-by: Daniel Diblik * Add new unit tests for InstallRhelKernel After Daniel's fix of the InstallRhelKernel, the new unit test are needed to cover those changes. * Don't use libraries for version comparison * drop the distutils and packaging libraries, use the compare_package_version function instead * address review comments Signed-off-by: Daniel Diblik * Fix message formatting, add debug, add test * fix the formatting of info message for conflicting kernel version * add debug loggerinst for highest found kernel version * add integration test verifying the latest kernel is installed after the conversion * revert if not non_rhel_kernels back to elif not non_rhel_kernels Signed-off-by: Daniel Diblik * Fix update RHEL kernel call Due regression RHELC-1707 was found, we needed to change place where kernel update is called. Without this change the system could end up with outdated kernel installed. * Minor changes to test_yum_check_update * use wildcard asterisk to check all kernel related packages * document which packages are verified Signed-off-by: Daniel Diblik --------- Signed-off-by: Daniel Diblik Co-authored-by: Adam Hosek --- .../conversion/preserve_only_rhel_kernel.py | 125 ++++++--- convert2rhel/pkghandler.py | 24 ++ .../preserve_only_rhel_kernel_test.py | 238 ++++++++++++------ convert2rhel/unit_tests/pkghandler_test.py | 51 ++++ .../common/checks-after-conversion/main.fmf | 11 +- .../test_yum_check_update.py | 9 + 6 files changed, 353 insertions(+), 105 deletions(-) create mode 100644 tests/integration/common/checks-after-conversion/test_yum_check_update.py diff --git a/convert2rhel/actions/conversion/preserve_only_rhel_kernel.py b/convert2rhel/actions/conversion/preserve_only_rhel_kernel.py index 60add7f5fe..86867a07e5 100644 --- a/convert2rhel/actions/conversion/preserve_only_rhel_kernel.py +++ b/convert2rhel/actions/conversion/preserve_only_rhel_kernel.py @@ -24,6 +24,8 @@ from convert2rhel.systeminfo import system_info +_kernel_update_needed = None + loggerinst = logging.getLogger(__name__) @@ -36,9 +38,13 @@ def run(self): super(InstallRhelKernel, self).run() loggerinst.task("Convert: Prepare kernel") + # Solution for RHELC-1707 + # Update is needed in the UpdateKernel action + global _kernel_update_needed # pylint: disable=global-statement + loggerinst.info("Installing RHEL kernel ...") output, ret_code = pkgmanager.call_yum_cmd(command="install", args=["kernel"]) - kernel_update_needed = False + _kernel_update_needed = False if ret_code != 0: self.set_result( @@ -50,37 +56,73 @@ def run(self): ) return - # Check if kernel with same version is already installed. + # Check which of the kernel versions are already installed. # Example output from yum and dnf: # "Package kernel-4.18.0-193.el8.x86_64 is already installed." - already_installed = re.search(r" (.*?)(?: is)? already installed", output, re.MULTILINE) - if already_installed: - rhel_kernel_nevra = already_installed.group(1) - non_rhel_kernels = pkghandler.get_installed_pkgs_w_different_fingerprint( - system_info.fingerprints_rhel, "kernel" + # When calling install, yum/dnf essentially reports all the already installed versions. + already_installed = re.findall(r" (.*?)(?: is)? already installed", output, re.MULTILINE) + # Get list of kernel pkgs not signed by Red Hat + non_rhel_kernels_pkg_info = pkghandler.get_installed_pkgs_w_different_fingerprint( + system_info.fingerprints_rhel, "kernel" + ) + # Extract the NEVRA from the package object to a list + non_rhel_kernels = [pkghandler.get_pkg_nevra(kernel) for kernel in non_rhel_kernels_pkg_info] + rhel_kernels = [kernel for kernel in already_installed if kernel not in non_rhel_kernels] + + # There is no RHEL kernel installed on the system at this point. + # Generally that would mean, that there is either only one kernel + # package installed on the system by the time of the conversion. + # Or none of the kernel packages installed is possible to be handled + # during the main transaction. + if not rhel_kernels: + info_message = ( + "Conflict of kernels: The running kernel has the same version as the latest RHEL kernel.\n" + "The kernel package could not be replaced during the main transaction.\n" + "We will try to install a lower version of the package,\n" + "remove the conflicting kernel and then update to the latest security patched version." ) - for non_rhel_kernel in non_rhel_kernels: - # We're comparing to NEVRA since that's what yum/dnf prints out - if rhel_kernel_nevra == pkghandler.get_pkg_nevra(non_rhel_kernel): - # If the installed kernel is from a third party (non-RHEL) and has the same NEVRA as the one available - # from RHEL repos, it's necessary to install an older version RHEL kernel and the third party one will - # be removed later in the conversion process. It's because yum/dnf is unable to reinstall a kernel. - info_message = ( - "Conflict of kernels: One of the installed kernels" - " has the same version as the latest RHEL kernel." - ) - loggerinst.info("\n%s" % info_message) - self.add_message( - level="INFO", - id="CONFLICT_OF_KERNELS", - title="Conflict of installed kernel versions", - description=info_message, - ) - pkghandler.handle_no_newer_rhel_kernel_available() - kernel_update_needed = True - - if kernel_update_needed: - pkghandler.update_rhel_kernel() + loggerinst.info("\n%s" % info_message) + self.add_message( + level="INFO", + id="CONFLICT_OF_KERNELS", + title="Conflict of installed kernel versions", + description=info_message, + ) + pkghandler.handle_no_newer_rhel_kernel_available() + _kernel_update_needed = True + + # In this case all kernel packages were already replaced during the main transaction. + # Having elif here to prevent breaking the action. Otherwise, when the first condition applies, + # and the pkghandler.handle_no_newer_rhel_kernel_available() we can assume the Action finished. + elif not non_rhel_kernels: + return + + # At this point we need to decide if the highest package version in the rhel_kernels list + # is higher than the highest package version in the non_rhel_kernels list + else: + latest_installed_non_rhel_kernel = pkghandler.get_highest_package_version( + ("non-RHEL kernel", non_rhel_kernels) + ) + loggerinst.debug( + "Latest installed kernel version from the original vendor: %s" % latest_installed_non_rhel_kernel + ) + latest_installed_rhel_kernel = pkghandler.get_highest_package_version(("RHEL kernel", rhel_kernels)) + loggerinst.debug("Latest installed RHEL kernel version: %s" % latest_installed_rhel_kernel) + is_rhel_kernel_higher = pkghandler.compare_package_versions( + latest_installed_rhel_kernel, latest_installed_non_rhel_kernel + ) + + # If the highest version of the RHEL kernel package installed at this point is indeed + # higher than any non-RHEL package, we don't need to do anything else. + if is_rhel_kernel_higher == 1: + return + + # This also contains a scenario, where the running non-RHEL kernel is of a higher version + # than the latest one available in the RHEL repositories. + # That might happen and happened before, when the original vendor patches the package + # with a higher release number. + pkghandler.handle_no_newer_rhel_kernel_available() + _kernel_update_needed = True class VerifyRhelKernelInstalled(actions.Action): @@ -145,12 +187,12 @@ def run(self): loggerinst.debug("Removing boot entry %s" % entry) os.remove(entry) - # Removing a boot entry that used to be the default makes grubby to choose a different entry as default, but we will - # call grub --set-default to set the new default on all the proper places, e.g. for grub2-editenv + # Removing a boot entry that used to be the default makes grubby to choose a different entry as default, + # but we will call grub --set-default to set the new default on all the proper places, e.g. for grub2-editenv output, ret_code = utils.run_subprocess(["/usr/sbin/grubby", "--default-kernel"], print_output=False) if ret_code: - # Not setting the default entry shouldn't be a deal breaker and the reason to stop the conversions, grub should - # pick one entry in any case. + # Not setting the default entry shouldn't be a deal breaker and the reason to stop the conversions, + # grub should pick one entry in any case. description = "Couldn't get the default GRUB2 boot loader entry:\n%s" % output loggerinst.warning(description) self.add_message( @@ -256,3 +298,20 @@ def install_additional_rhel_kernel_pkgs(self, additional_pkgs): if name != "kernel": loggerinst.info("Installing RHEL %s" % name) pkgmanager.call_yum_cmd("install", args=[name]) + + +class UpdateKernel(actions.Action): + id = "UPDATE_KERNEL" + dependencies = ("FIX_DEFAULT_KERNEL",) + + def run(self): + super(UpdateKernel, self).run() + # Solution for RHELC-1707 + # This variable is set in the InstallRhelKernel action + global _kernel_update_needed + + if _kernel_update_needed: + # Note: Info message is in the function + pkghandler.update_rhel_kernel() + else: + loggerinst.info("RHEL kernel already present in latest version. Update not needed.\n") diff --git a/convert2rhel/pkghandler.py b/convert2rhel/pkghandler.py index 036de82b60..ce7a19cd24 100644 --- a/convert2rhel/pkghandler.py +++ b/convert2rhel/pkghandler.py @@ -1056,3 +1056,27 @@ def _parse_pkg_with_dnf(pkg): pkg_ver_components = (name, epoch, version, release, arch) return pkg_ver_components + + +def get_highest_package_version(pkgs): + """ + Get the highest version from the provided list of packages. + :param pkgs: A tuple containing the name of the package list (as a string) and the list of package versions (as a list of strings). + :type pkgs: tuple(str,list[str]) + :return: Return a single package with the highest version. + :rtype: str + + :raises ValueError: If the list is empty, raise a ValueError. + """ + name, nevra_list = pkgs + + if not nevra_list: + loggerinst.debug("The list of %s packages is empty." % name) + raise ValueError + + highest_version = nevra_list[0] + + for nevra in nevra_list[1:]: + highest_version = nevra if compare_package_versions(nevra, highest_version) == 1 else highest_version + + return highest_version diff --git a/convert2rhel/unit_tests/actions/conversion/preserve_only_rhel_kernel_test.py b/convert2rhel/unit_tests/actions/conversion/preserve_only_rhel_kernel_test.py index eef2cb1d78..b08d21e1e5 100644 --- a/convert2rhel/unit_tests/actions/conversion/preserve_only_rhel_kernel_test.py +++ b/convert2rhel/unit_tests/actions/conversion/preserve_only_rhel_kernel_test.py @@ -66,107 +66,184 @@ def kernel_packages_install_instance(): return preserve_only_rhel_kernel.KernelPkgsInstall() +@pytest.fixture +def update_kernel_instance(): + return preserve_only_rhel_kernel.UpdateKernel() + + class TestInstallRhelKernel: @pytest.mark.parametrize( ( "subprocess_output", - "is_only_rhel_kernel", - "expected", + "pkgs_w_diff_fingerprint", + "no_newer_kernel_call", + "update_kernel", + "action_message", + "action_result", ), ( - ("Package kernel-3.10.0-1127.19.1.el7.x86_64 already installed and latest version", True, False), - ("Package kernel-3.10.0-1127.19.1.el7.x86_64 already installed and latest version", False, True), - ("Installed:\nkernel", False, False), - ), - ids=( - "Kernels collide and installed is already RHEL. Do not update.", - "Kernels collide and installed is not RHEL and older. Update.", - "Kernels do not collide. Install RHEL kernel and do not update.", + ( + # Info about installed kernel from yum contains the same version as is listed in the different fingerprint pkgs + # The latest installed kernel is from CentOS + "Package kernel-4.18.0-193.el8.x86_64 is already installed.", + [ + create_pkg_information( + name="kernel", + version="4.18.0", + release="193.el8", + arch="x86_64", + packager="CentOS", + ), + create_pkg_information( + name="kernel", + version="4.18.0", + release="183.el8", + arch="x86_64", + packager="CentOS", + ), + ], + 1, + True, + set( + ( + actions.ActionMessage( + level="INFO", + id="CONFLICT_OF_KERNELS", + title="Conflict of installed kernel versions", + description="Conflict of kernels: The running kernel has the same version as the latest RHEL kernel.\n" + "The kernel package could not be replaced during the main transaction.\n" + "We will try to install a lower version of the package,\n" + "remove the conflicting kernel and then update to the latest security patched version.", + ), + ), + ), + actions.ActionResult(level="SUCCESS", id="SUCCESS"), + ), + ( + # Output from yum contains different version than is listed in different fingerprint + # Rhel kernel already installed with centos kernels + "Package kernel-4.18.0-205.el8.x86_64 is already installed.", + [ + create_pkg_information( + name="kernel", + version="4.18.0", + release="193.el8", + arch="x86_64", + packager="CentOS", + ), + create_pkg_information( + name="kernel", + version="4.18.0", + release="183.el8", + arch="x86_64", + packager="CentOS", + ), + ], + 0, + False, + set(()), + actions.ActionResult(level="SUCCESS", id="SUCCESS"), + ), + ( + # Only rhel kernel already installed + "Package kernel-4.18.0-205.el8.x86_64 is already installed.", + [], + 0, + False, + set(()), + actions.ActionResult(level="SUCCESS", id="SUCCESS"), + ), + ( + # Output from yum contains different version than is listed in different fingerprint + # Rhel kernel already installed in older versin than centos kernel + "Package kernel-4.18.0-183.el8.x86_64 is already installed.", + [ + create_pkg_information( + name="kernel", + version="4.18.0", + release="193.el8", + arch="x86_64", + packager="CentOS", + ), + ], + 1, + True, + set(()), + actions.ActionResult(level="SUCCESS", id="SUCCESS"), + ), ), ) - @centos7 + @centos8 def test_install_rhel_kernel( - self, subprocess_output, is_only_rhel_kernel, expected, pretend_os, install_rhel_kernel_instance, monkeypatch + self, + monkeypatch, + subprocess_output, + pkgs_w_diff_fingerprint, + install_rhel_kernel_instance, + no_newer_kernel_call, + update_kernel, + pretend_os, + action_message, + action_result, ): - update_rhel_kernel_mock = mock.Mock() + """Test the logic of kernel installation&update""" + handle_no_newer_rhel_kernel_available = mock.Mock() monkeypatch.setattr( utils, "run_subprocess", RunSubprocessMocked(return_string=subprocess_output, return_code=0) ) - monkeypatch.setattr(pkghandler, "handle_no_newer_rhel_kernel_available", mock.Mock()) - monkeypatch.setattr(pkghandler, "update_rhel_kernel", value=update_rhel_kernel_mock) - - pkg_selection = "empty" if is_only_rhel_kernel else "kernels" monkeypatch.setattr( pkghandler, "get_installed_pkgs_w_different_fingerprint", - GetInstalledPkgsWDifferentFingerprintMocked(pkg_selection=pkg_selection), + GetInstalledPkgsWDifferentFingerprintMocked(return_value=pkgs_w_diff_fingerprint), ) + monkeypatch.setattr(pkghandler, "handle_no_newer_rhel_kernel_available", handle_no_newer_rhel_kernel_available) + install_rhel_kernel_instance.run() - if expected: - update_rhel_kernel_mock.assert_called_once() + + assert handle_no_newer_rhel_kernel_available.call_count == no_newer_kernel_call + assert preserve_only_rhel_kernel._kernel_update_needed == update_kernel + assert action_message.issuperset(install_rhel_kernel_instance.messages) + assert action_message.issubset(install_rhel_kernel_instance.messages) + assert action_result == install_rhel_kernel_instance.result @pytest.mark.parametrize( - ("subprocess_output",), + ("subprocess_output", "subprocess_return", "action_message", "action_result"), ( - ("Package kernel-2.6.32-754.33.1.el7.x86_64 already installed and latest version",), - ("Package kernel-4.18.0-193.el8.x86_64 is already installed.",), + ( + "yum command failed", + 1, + set(()), + actions.ActionResult( + level="ERROR", + id="FAILED_TO_INSTALL_RHEL_KERNEL", + title="Failed to install RHEL kernel", + description="There was an error while attempting to install the RHEL kernel from yum.", + remediations="Please check that you can access the repositories that provide the RHEL kernel.", + ), + ), ), ) - @centos7 - def test_install_rhel_kernel_already_installed_regexp( - self, subprocess_output, pretend_os, monkeypatch, install_rhel_kernel_instance + @centos8 + def test_install_rhel_kernel_yum_fail( + self, + monkeypatch, + subprocess_output, + subprocess_return, + action_message, + action_result, + install_rhel_kernel_instance, + pretend_os, ): - monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked(return_string=subprocess_output)) monkeypatch.setattr( - pkghandler, - "get_installed_pkgs_w_different_fingerprint", - GetInstalledPkgsWDifferentFingerprintMocked(pkg_selection="kernels"), + utils, "run_subprocess", RunSubprocessMocked(return_string=subprocess_output, return_code=subprocess_return) ) install_rhel_kernel_instance.run() - assert pkghandler.get_installed_pkgs_w_different_fingerprint.call_count == 1 - - @centos7 - def test_install_rhel_kernel_error(self, pretend_os, install_rhel_kernel_instance, monkeypatch): - - monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked(return_code=1)) - install_rhel_kernel_instance.run() - unit_tests.assert_actions_result( - install_rhel_kernel_instance, - level="ERROR", - id="FAILED_TO_INSTALL_RHEL_KERNEL", - title="Failed to install RHEL kernel", - description="There was an error while attempting to install the RHEL kernel from yum.", - remediations="Please check that you can access the repositories that provide the RHEL kernel.", - ) - - @centos7 - def test_install_rhel_kernel_info_msg(self, pretend_os, install_rhel_kernel_instance, monkeypatch): - subprocess_output = "Package kernel-3.10.0-1127.19.1.el7.x86_64 already installed and latest version" - monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked(return_string=subprocess_output)) - monkeypatch.setattr(pkghandler, "handle_no_newer_rhel_kernel_available", mock.Mock()) - monkeypatch.setattr( - pkghandler, - "get_installed_pkgs_w_different_fingerprint", - GetInstalledPkgsWDifferentFingerprintMocked(pkg_selection="kernels"), - ) - install_rhel_kernel_instance.run() - expected = set( - ( - actions.ActionMessage( - level="INFO", - id="CONFLICT_OF_KERNELS", - title="Conflict of installed kernel versions", - description="Conflict of kernels: One of the installed kernels has the same version as the latest RHEL kernel.", - diagnosis=None, - remediations=None, - ), - ) - ) - assert expected.issuperset(install_rhel_kernel_instance.messages) - assert expected.issubset(install_rhel_kernel_instance.messages) + assert action_message.issuperset(install_rhel_kernel_instance.messages) + assert action_message.issubset(install_rhel_kernel_instance.messages) + assert action_result == install_rhel_kernel_instance.result class TestKernelPkgsInstall: @@ -524,3 +601,22 @@ def test_fix_default_kernel_with_no_incorrect_kernel( for record in info_records: assert not re.search("Boot kernel [^ ]\\+ was changed to [^ ]\\+", record.message) + + +class TestUpdateKernel: + @pytest.mark.parametrize( + ("update_kernel"), + ( + (True), + (False), + ), + ) + @centos8 + def test_update_kernel(self, monkeypatch, update_kernel_instance, update_kernel, pretend_os): + preserve_only_rhel_kernel._kernel_update_needed = update_kernel + update_rhel_kernel = mock.Mock() + monkeypatch.setattr(pkghandler, "update_rhel_kernel", update_rhel_kernel) + + update_kernel_instance.run() + + assert (update_rhel_kernel.call_count == 1) == update_kernel diff --git a/convert2rhel/unit_tests/pkghandler_test.py b/convert2rhel/unit_tests/pkghandler_test.py index 00b335a2b5..5857d2b7dd 100644 --- a/convert2rhel/unit_tests/pkghandler_test.py +++ b/convert2rhel/unit_tests/pkghandler_test.py @@ -1797,3 +1797,54 @@ def test_get_package_repositories_repoquery_failure(pretend_os, monkeypatch, cap for package, repo in result.items(): assert package in packages assert repo == "N/A" + + +@pytest.mark.parametrize( + ( + "packages", + "expected", + ), + [ + ( + ( + "kernels", + [ + "kernel-0:6.10.5-500.fc40.x86_64", + "kernel-0:6.8.5-301.fc40.x86_64", + "kernel-0:6.8.6-301.fc40.x86_64", + "kernel-0:6.10.6-200.fc40.x86_64", + ], + ), + "kernel-0:6.10.6-200.fc40.x86_64", + ), + ( + ( + "kernels", + [ + "kernel-0:6.8.5-301.fc40.x86_64", + ], + ), + "kernel-0:6.8.5-301.fc40.x86_64", + ), + ( + ( + "kernels", + [ + "kernel-0:12.8.5-301.fc40.x86_64", + "kernel-1:1.1.5-101.fc40.x86_64", + ], + ), + "kernel-1:1.1.5-101.fc40.x86_64", + ), + ], +) +def test_get_highest_package_version(packages, expected): + result = pkghandler.get_highest_package_version(pkgs=packages) + + assert result == expected + + +@pytest.mark.parametrize("packages", ("void", [])) +def test_get_highest_package_version_value_err(packages): + with pytest.raises(ValueError): + pkghandler.get_highest_package_version(pkgs=packages) diff --git a/tests/integration/common/checks-after-conversion/main.fmf b/tests/integration/common/checks-after-conversion/main.fmf index cdca5d07ea..cdc447e370 100644 --- a/tests/integration/common/checks-after-conversion/main.fmf +++ b/tests/integration/common/checks-after-conversion/main.fmf @@ -148,7 +148,7 @@ order: 52 summary+: | Run yum check description+: | - After conversion check verifying yum check is able to finis without any issues. + After conversion verify that yum check is able to finish without any issues. test: pytest -m test_yum_check /check_firewalld_errors: @@ -157,3 +157,12 @@ order: 52 description+: | Verify that firewalld is not reporting any issues in the logs. test: pytest -m test_check_firewalld_errors + +/yum_check_update: + summary+: | + Run yum check-update + description+: | + After the conversion verify yum check-update does not return outdated package. + Validated packages: + - kernel* + test: pytest -m test_yum_check_update diff --git a/tests/integration/common/checks-after-conversion/test_yum_check_update.py b/tests/integration/common/checks-after-conversion/test_yum_check_update.py new file mode 100644 index 0000000000..25696f9918 --- /dev/null +++ b/tests/integration/common/checks-after-conversion/test_yum_check_update.py @@ -0,0 +1,9 @@ +import pytest + + +@pytest.mark.parametrize("package", ["kernel*"]) +def test_yum_check_update(shell, package): + """ + After the conversion verify yum check-update does not return outdated package. + """ + assert package not in shell(f"yum check-update {package}").output