diff --git a/convert2rhel/checks.py b/convert2rhel/checks.py index a2605de141..0ea4d3762f 100644 --- a/convert2rhel/checks.py +++ b/convert2rhel/checks.py @@ -15,7 +15,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - import itertools import logging import os @@ -28,6 +27,7 @@ from convert2rhel import __version__ as convert2rhel_version from convert2rhel import grub, pkgmanager, utils from convert2rhel.pkghandler import ( + _package_version_cmp, call_yum_cmd, compare_package_versions, get_installed_pkg_objects, @@ -47,6 +47,7 @@ BAD_KERNEL_RELEASE_SUBSTRINGS = ("uek", "rt", "linode") LINK_KMODS_RH_POLICY = "https://access.redhat.com/third-party-software-support" +LINK_PREVENT_KMODS_FROM_LOADING = "https://access.redhat.com/solutions/41278" # The kernel version stays the same throughout a RHEL major version COMPATIBLE_KERNELS_VERS = { 6: "2.6.32", @@ -54,8 +55,46 @@ 8: "4.18.0", } +# Python 2.6 compatibility. +# This code is copied from Pthon-3.10's functools module, +# licensed under the Python Software Foundation License, version 2 +try: + from functools import cmp_to_key +except ImportError: + + def cmp_to_key(mycmp): + """Convert a cmp= function into a key= function""" + + class K(object): + __slots__ = ["obj"] + + def __init__(self, obj): + self.obj = obj + + def __lt__(self, other): + return mycmp(self.obj, other.obj) < 0 + + def __gt__(self, other): + return mycmp(self.obj, other.obj) > 0 + + def __eq__(self, other): + return mycmp(self.obj, other.obj) == 0 + + def __le__(self, other): + return mycmp(self.obj, other.obj) <= 0 + + def __ge__(self, other): + return mycmp(self.obj, other.obj) >= 0 + + __hash__ = None + + return K -def perform_pre_checks(): + +# End of PSF Licensed code + + +def perform_system_checks(): """Early checks after system facts should be added here.""" check_custom_repos_are_valid() @@ -202,18 +241,20 @@ def check_tainted_kmods(): multipath 20480 0 - Live 0x0000000000000000 linear 20480 0 - Live 0x0000000000000000 system76_io 16384 0 - Live 0x0000000000000000 (OE) <<<<<< Tainted - system76_acpi 16384 0 - Live 0x0000000000000000 (OE) <<<<< Tainted + system76_acpi 16384 0 - Live 0x0000000000000000 (OE) <<<<<< Tainted """ logger.task("Prepare: Checking if loaded kernel modules are not tainted") unsigned_modules, _ = run_subprocess(["grep", "(", "/proc/modules"]) module_names = "\n ".join([mod.split(" ")[0] for mod in unsigned_modules.splitlines()]) if unsigned_modules: logger.critical( - "Tainted kernel module(s) detected. " + "Tainted kernel modules detected:\n {0}\n" "Third-party components are not supported per our " - "software support policy\n{0}\n\n" - "Uninstall or disable the following module(s) and run convert2rhel " - "again to continue with the conversion:\n {1}".format(LINK_KMODS_RH_POLICY, module_names) + "software support policy:\n {1}\n" + "Prevent the modules from loading by following {2}" + " and run convert2rhel again to continue with the conversion.".format( + module_names, LINK_KMODS_RH_POLICY, LINK_PREVENT_KMODS_FROM_LOADING + ) ) logger.info("No tainted kernel module is loaded.") @@ -261,7 +302,9 @@ def check_custom_repos_are_valid(): return output, ret_code = call_yum_cmd( - command="makecache", args=["-v", "--setopt=*.skip_if_unavailable=False"], print_output=False + command="makecache", + args=["-v", "--setopt=*.skip_if_unavailable=False"], + print_output=False, ) if ret_code != 0: logger.critical( @@ -274,11 +317,19 @@ def check_custom_repos_are_valid(): def ensure_compatibility_of_kmods(): - """Ensure if the host kernel modules are compatible with RHEL.""" + """Ensure that the host kernel modules are compatible with RHEL. + + :raises SystemExit: Interrupts the conversion because some kernel modules are not supported in RHEL. + """ host_kmods = get_loaded_kmods() rhel_supported_kmods = get_rhel_supported_kmods() unsupported_kmods = get_unsupported_kmods(host_kmods, rhel_supported_kmods) - if unsupported_kmods: + + # Validate the best case first. If we don't have any unsupported_kmods, this means + # that everything is compatible and good to go. + if not unsupported_kmods: + logger.debug("All loaded kernel modules are available in RHEL.") + else: not_supported_kmods = "\n".join( map( lambda kmod: "/lib/modules/{kver}/{kmod}".format(kver=system_info.booted_kernel, kmod=kmod), @@ -286,14 +337,13 @@ def ensure_compatibility_of_kmods(): ) ) logger.critical( - ( - "The following kernel modules are not supported in RHEL:\n{kmods}\n" - "Make sure you have updated the kernel to the latest available version and rebooted the system. " - "Remove the unsupported modules and run convert2rhel again to continue with the conversion." - ).format(kmods=not_supported_kmods, system=system_info.name) + "The following loaded kernel modules are not available in RHEL:\n{0}\n" + "First, make sure you have updated the kernel to the latest available version and rebooted the system.\n" + "If this message appears again after doing the above, prevent the modules from loading by following {1}" + " and run convert2rhel again to continue with the conversion.".format( + "\n".join(not_supported_kmods), LINK_PREVENT_KMODS_FROM_LOADING + ) ) - else: - logger.info("Kernel modules are compatible.") def validate_package_manager_transaction(): @@ -391,64 +441,46 @@ def get_most_recent_unique_kernel_pkgs(pkgs): kernel pkg do not deprecate kernel modules we only select the most recent ones. - All RHEL kmods packages starts with kernel* or kmod* - - For example, we have the following packages list: - kernel-core-0:4.18.0-240.10.1.el8_3.x86_64 - kernel-core-0:4.19.0-240.10.1.el8_3.x86_64 - kmod-debug-core-0:4.18.0-240.10.1.el8_3.x86_64 - kmod-debug-core-0:4.18.0-245.10.1.el8_3.x86_64 - ==> (output of this function will be) - kernel-core-0:4.19.0-240.10.1.el8_3.x86_64 - kmod-debug-core-0:4.18.0-245.10.1.el8_3.x86_64 - - _repos_version_key extract the version of a package - into the tuple, i.e. - kernel-core-0:4.18.0-240.10.1.el8_3.x86_64 ==> - (4, 15, 0, 240, 10, 1) - - - :type pkgs: Iterable[str] - :type pkgs_groups: - Iterator[ - Tuple[ - package_name_without_version, - Iterator[package_name, ...], - ..., - ] + .. note:: + All RHEL kmods packages starts with kernel* or kmod* + + For example, consider the following list of packages:: + + list_of_pkgs = [ + 'kernel-core-0:4.18.0-240.10.1.el8_3.x86_64', + 'kernel-core-0:4.19.0-240.10.1.el8_3.x86_64', + 'kmod-debug-core-0:4.18.0-240.10.1.el8_3.x86_64', + 'kmod-debug-core-0:4.18.0-245.10.1.el8_3.x86_64 ] - """ - pkgs_groups = itertools.groupby(sorted(pkgs), lambda pkg_name: pkg_name.split(":")[0]) - return tuple( - max(distinct_kernel_pkgs[1], key=_repos_version_key) - for distinct_kernel_pkgs in pkgs_groups - if distinct_kernel_pkgs[0].startswith(("kernel", "kmod")) - ) + And when this function gets called with that same list of packages, + we have the following output:: + result = get_most_recent_unique_kernel_pkgs(pkgs=list_of_pkgs) + print(result) + # ( + # 'kernel-core-0:4.19.0-240.10.1.el8_3.x86_64', + # 'kmod-debug-core-0:4.18.0-245.10.1.el8_3.x86_64' + # ) -def _repos_version_key(pkg_name): - try: - rpm_version = KERNEL_REPO_RE.search(pkg_name).group("version") - except AttributeError: - logger.critical( - "Unexpected package:\n%s\n is a source of kernel modules.", - pkg_name, - ) - else: - return tuple( - map( - _convert_to_int_or_zero, - KERNEL_REPO_VER_SPLIT_RE.split(rpm_version), - ) - ) + :param pkgs: A list of package names to be analyzed. + :type pkgs: list[str] + :return: A tuple of packages name sorted and normalized + :rtype: tuple[str] + """ + pkgs_groups = itertools.groupby(sorted(pkgs), lambda pkg_name: pkg_name.split(":")[0]) + list_of_sorted_pkgs = [] + for distinct_kernel_pkgs in pkgs_groups: + if distinct_kernel_pkgs[0].startswith(("kernel", "kmod")): + list_of_sorted_pkgs.append( + max( + distinct_kernel_pkgs[1], + key=cmp_to_key(_package_version_cmp), + ) + ) -def _convert_to_int_or_zero(s): - try: - return int(s) - except ValueError: - return 0 + return tuple(list_of_sorted_pkgs) def get_rhel_kmods_keys(rhel_kmods_str): @@ -462,11 +494,19 @@ def get_rhel_kmods_keys(rhel_kmods_str): def get_unsupported_kmods(host_kmods, rhel_supported_kmods): - """Return a set of those installed kernel modules that are not available in RHEL repositories. + """Return a set of full paths to those installed kernel modules that are + not available in RHEL repositories. - Ignore certain kmods mentioned in the system configs. These kernel modules moved to kernel core, meaning that the - functionality is retained and we would be incorrectly saying that the modules are not supported in RHEL.""" - return host_kmods - rhel_supported_kmods - set(system_info.kmods_to_ignore) + Ignore certain kmods mentioned in the system configs. These kernel modules + moved to kernel core, meaning that the functionality is retained and we + would be incorrectly saying that the modules are not supported in RHEL. + """ + unsupported_kmods_subpaths = host_kmods - rhel_supported_kmods - set(system_info.kmods_to_ignore) + unsupported_kmods_full_paths = [ + "/lib/modules/{kver}/{kmod}".format(kver=system_info.booted_kernel, kmod=kmod) + for kmod in unsupported_kmods_subpaths + ] + return unsupported_kmods_full_paths def check_rhel_compatible_kernel_is_used(): @@ -527,7 +567,10 @@ def _bad_kernel_version(kernel_release): def _bad_kernel_package_signature(kernel_release): """Return True if the booted kernel is not signed by the original OS vendor, i.e. it's a custom kernel.""" vmlinuz_path = "/boot/vmlinuz-%s" % kernel_release - kernel_pkg, return_code = run_subprocess(["rpm", "-qf", "--qf", "%{NAME}", vmlinuz_path], print_output=False) + kernel_pkg, return_code = run_subprocess( + ["rpm", "-qf", "--qf", "%{NAME}", vmlinuz_path], + print_output=False, + ) logger.debug("Booted kernel package name: {0}".format(kernel_pkg)) os_vendor = system_info.name.split()[0] diff --git a/convert2rhel/main.py b/convert2rhel/main.py index e6dad54574..70f467d773 100644 --- a/convert2rhel/main.py +++ b/convert2rhel/main.py @@ -97,7 +97,7 @@ def main(): pkghandler.clean_yum_metadata() # check the system prior the conversion (possible inhibit) - checks.perform_pre_checks() + checks.perform_system_checks() # backup system release file before starting conversion process loggerinst.task("Prepare: Backup System") diff --git a/convert2rhel/pkghandler.py b/convert2rhel/pkghandler.py index 683a55d5c3..596679c873 100644 --- a/convert2rhel/pkghandler.py +++ b/convert2rhel/pkghandler.py @@ -1111,3 +1111,54 @@ def clean_yum_metadata(): return loggerinst.info("Cached yum metadata cleaned successfully.") + + +def _package_version_cmp(pkg_1, pkg_2): + """Compare the version key in a given package name. + + Consider the following variables that will be passed to this function:: + + pkg_1 = 'kernel-core-0:4.18.0-240.10.1.el8_3.x86_64' + pkg_2 = 'kernel-core-0:4.18.0-239.0.0.el8_3.x86_64' + + The output of this will be a tuple containing the package version in a + tuple:: + + result = _package_version_cmp(pkg_1, pkg_2) + print("Result is: %s" % result) + # Result is: -1 + + The function will ignore the package name as it is not an important + information here and will only care about the version that is tied to it's + name. + + :param pkg_1: The first package to extract the version + :type pkg_1: str + :param pkg_2: The second package to extract the version + :type pkg_2: str + :return: An integer resulting in the package comparision + :rtype: int + """ + + # TODO(r0x0d): This function still needs some enhancements code-wise, it + # workes perfectly, but the way we are handling the versions is not 100% + # complete yet. will be done in a future work. Right now, all the list of + # changes are listed in this comment: + # https://github.com/oamg/convert2rhel/pull/469#discussion_r873971400 + pkg_ver_components = [] + for pkg in pkg_1, pkg_2: + # Remove the package name and split the rest between epoch + version + # and release + arch + epoch_version, release_arch = pkg.rsplit("-", 2)[-2:] + # Separate the (optional) epoch from the version + epoch_version = epoch_version.split(":", 1) + if len(epoch_version) == 1: + epoch = "0" + version = epoch_version[0] + else: + epoch, version = epoch_version + # Discard the arch + release = release_arch.rsplit(".", 1)[0] + pkg_ver_components.append((epoch, version, release)) + + return rpm.labelCompare(pkg_ver_components[0], pkg_ver_components[1]) diff --git a/convert2rhel/systeminfo.py b/convert2rhel/systeminfo.py index 8f2d95f500..f6892e445b 100644 --- a/convert2rhel/systeminfo.py +++ b/convert2rhel/systeminfo.py @@ -332,12 +332,18 @@ def is_rpm_installed(name): _, return_code = run_subprocess(["rpm", "-q", name], print_cmd=False, print_output=False) return return_code == 0 - # TODO write unit tests def get_enabled_rhel_repos(self): - """Return a tuple of repoids containing RHEL packages. + """Get a list of enabled repositories containing RHEL packages. - These are either the repos enabled through RHSM or the custom - repositories passed though CLI. + This function can return either the repositories enabled throught the RHSM tool during the conversion or, if + the user manually specified the repositories throught the CLI, it will return them based on the + `tool_opts.enablerepo` option. + + .. note:: + The repositories passed through the CLI have more priority than the ones get get from RHSM. + + :return: A list of enabled repos to use during the conversion + :rtype: list[str] """ # TODO: # if not self.submgr_enabled_repos: diff --git a/convert2rhel/unit_tests/checks_test.py b/convert2rhel/unit_tests/checks_test.py index 8a00a2e23f..3bb0c063cb 100644 --- a/convert2rhel/unit_tests/checks_test.py +++ b/convert2rhel/unit_tests/checks_test.py @@ -157,7 +157,7 @@ def test_perform_pre_checks(monkeypatch): monkeypatch.setattr(checks, "is_loaded_kernel_latest", value=is_loaded_kernel_latest_mock) monkeypatch.setattr(checks, "check_dbus_is_running", value=check_dbus_is_running_mock) - checks.perform_pre_checks() + checks.perform_system_checks() check_convert2rhel_latest_mock.assert_called_once() check_thirdparty_kmods_mock.assert_called_once() @@ -169,7 +169,7 @@ def test_perform_pre_checks(monkeypatch): check_dbus_is_running_mock.assert_called_once() -def test_pre_ponr_checks(monkeypatch): +def test_perform_pre_ponr_checks(monkeypatch): ensure_compatibility_of_kmods_mock = mock.Mock() create_transaction_handler_mock = mock.Mock() monkeypatch.setattr( @@ -315,14 +315,14 @@ def test_c2r_up_to_date(caplog, monkeypatch, convert2rhel_latest_version_test): ( HOST_MODULES_STUB_GOOD, None, - "Kernel modules are compatible", + "loaded kernel modules are available in RHEL", None, ), ( HOST_MODULES_STUB_BAD, SystemExit, None, - "Kernel modules are compatible", + "loaded kernel modules are available in RHEL", ), ), ) @@ -383,13 +383,13 @@ def test_validate_package_manager_transaction(monkeypatch, caplog): # ff-memless specified to be ignored in the config, so no exception raised ( "kernel/drivers/input/ff-memless.ko.xz", - "Kernel modules are compatible", - "The following kernel modules are not supported in RHEL", + "loaded kernel modules are available in RHEL", + "The following loaded kernel modules are not available in RHEL", None, ), ( "kernel/drivers/input/other.ko.xz", - "The following kernel modules are not supported in RHEL", + "The following loaded kernel modules are not available in RHEL", None, SystemExit, ), @@ -477,9 +477,18 @@ def test_get_loaded_kmods(monkeypatch): 0, ), ), - (("modinfo", "-F", "filename", "a"), (MODINFO_STUB.split()[0] + "\n", 0)), - (("modinfo", "-F", "filename", "b"), (MODINFO_STUB.split()[1] + "\n", 0)), - (("modinfo", "-F", "filename", "c"), (MODINFO_STUB.split()[2] + "\n", 0)), + ( + ("modinfo", "-F", "filename", "a"), + (MODINFO_STUB.split()[0] + "\n", 0), + ), + ( + ("modinfo", "-F", "filename", "b"), + (MODINFO_STUB.split()[1] + "\n", 0), + ), + ( + ("modinfo", "-F", "filename", "c"), + (MODINFO_STUB.split()[2] + "\n", 0), + ), ), ) monkeypatch.setattr( @@ -491,10 +500,10 @@ def test_get_loaded_kmods(monkeypatch): @pytest.mark.parametrize( - ("repoquery_f_stub", "repoquery_l_stub", "exception"), + ("repoquery_f_stub", "repoquery_l_stub"), ( - (REPOQUERY_F_STUB_GOOD, REPOQUERY_L_STUB_GOOD, None), - (REPOQUERY_F_STUB_BAD, REPOQUERY_L_STUB_GOOD, SystemExit), + (REPOQUERY_F_STUB_GOOD, REPOQUERY_L_STUB_GOOD), + (REPOQUERY_F_STUB_BAD, REPOQUERY_L_STUB_GOOD), ), ) @centos8 @@ -503,7 +512,6 @@ def test_get_rhel_supported_kmods( pretend_os, repoquery_f_stub, repoquery_l_stub, - exception, ): run_subprocess_mock = mock.Mock( side_effect=run_subprocess_side_effect( @@ -522,24 +530,21 @@ def test_get_rhel_supported_kmods( "run_subprocess", value=run_subprocess_mock, ) - if exception: - with pytest.raises(exception): - checks.get_rhel_supported_kmods() - else: - res = checks.get_rhel_supported_kmods() - assert res == set( - ( - "kernel/lib/a.ko", - "kernel/lib/a.ko.xz", - "kernel/lib/b.ko.xz", - "kernel/lib/c.ko.xz", - "kernel/lib/c.ko", - ) + + res = checks.get_rhel_supported_kmods() + assert res == set( + ( + "kernel/lib/a.ko", + "kernel/lib/a.ko.xz", + "kernel/lib/b.ko.xz", + "kernel/lib/c.ko.xz", + "kernel/lib/c.ko", ) + ) @pytest.mark.parametrize( - ("pkgs", "exp_res", "exception"), + ("pkgs", "exp_res"), ( ( ( @@ -552,7 +557,6 @@ def test_get_rhel_supported_kmods( "kernel-core-0:4.18.0-240.15.1.el8_3.x86_64", "kernel-debug-core-0:4.18.0-240.15.1.el8_3.x86_64", ), - None, ), ( ( @@ -560,7 +564,6 @@ def test_get_rhel_supported_kmods( "kmod-core-0:4.18.0-240.15.1.el8_3.x86_64", ), ("kmod-core-0:4.18.0-240.15.1.el8_3.x86_64",), - None, ), ( ( @@ -568,7 +571,6 @@ def test_get_rhel_supported_kmods( "kmod-core-0:4.18.0-240.15.1.el8_3.x86_64", ), ("kmod-core-0:4.18.0-240.15.1.el8_3.x86_64",), - None, ), ( ( @@ -576,7 +578,6 @@ def test_get_rhel_supported_kmods( "kernel-core-0:4.18.0-240.15.1.el8_3.x86_64", ), ("kernel-core-0:4.18.0-240.15.1.el8_3.x86_64",), - None, ), ( ( @@ -584,7 +585,6 @@ def test_get_rhel_supported_kmods( "kernel-core-0:4.18.0-240.15.1.el8_3.x86_64", ), ("kernel-core-0:4.18.0-240.15.1.el8_3.x86_64",), - None, ), ( ( @@ -592,27 +592,23 @@ def test_get_rhel_supported_kmods( "kernel-core-0:4.18.0-240.15.1.el8_3.x86_64", ), ("kernel-core-0:4.18.0-240.16.beta5.1.el8_3.x86_64",), - None, ), - (("kernel_bad_package:111111",), (), SystemExit), + (("kernel_bad_package:111111",), ("kernel_bad_package:111111",)), ( ( "kernel-core-0:4.18.0-240.15.1.el8_3.x86_64", "kernel_bad_package:111111", "kernel-core-0:4.18.0-240.15.1.el8_3.x86_64", ), - (), - SystemExit, + ( + "kernel-core-0:4.18.0-240.15.1.el8_3.x86_64", + "kernel_bad_package:111111", + ), ), ), ) -def test_get_most_recent_unique_kernel_pkgs(pkgs, exp_res, exception): - if not exception: - most_recent_pkgs = tuple(checks.get_most_recent_unique_kernel_pkgs(pkgs)) - assert exp_res == most_recent_pkgs - else: - with pytest.raises(exception): - tuple(checks.get_most_recent_unique_kernel_pkgs(pkgs)) +def test_get_most_recent_unique_kernel_pkgs(pkgs, exp_res): + assert tuple(checks.get_most_recent_unique_kernel_pkgs(pkgs)) == exp_res @pytest.mark.parametrize( @@ -733,7 +729,11 @@ def test_check_efi_old_sys(self): @unit_tests.mock(checks.system_info, "version", _gen_version(7, 9)) @unit_tests.mock(checks, "logger", GetLoggerMocked()) @unit_tests.mock(os.path, "exists", lambda x: not x == "/usr/sbin/efibootmgr") - @unit_tests.mock(grub, "EFIBootInfo", EFIBootInfoMocked(exception=grub.BootloaderError("errmsg"))) + @unit_tests.mock( + grub, + "EFIBootInfo", + EFIBootInfoMocked(exception=grub.BootloaderError("errmsg")), + ) def test_check_efi_efi_detected_without_efibootmgr(self): self._check_efi_critical("Install efibootmgr to continue converting the UEFI-based system.") @@ -743,7 +743,11 @@ def test_check_efi_efi_detected_without_efibootmgr(self): @unit_tests.mock(checks.system_info, "version", _gen_version(7, 9)) @unit_tests.mock(checks, "logger", GetLoggerMocked()) @unit_tests.mock(os.path, "exists", lambda x: x == "/usr/sbin/efibootmgr") - @unit_tests.mock(grub, "EFIBootInfo", EFIBootInfoMocked(exception=grub.BootloaderError("errmsg"))) + @unit_tests.mock( + grub, + "EFIBootInfo", + EFIBootInfoMocked(exception=grub.BootloaderError("errmsg")), + ) def test_check_efi_efi_detected_non_intel(self): self._check_efi_critical("Only x86_64 systems are supported for UEFI conversions.") @@ -753,7 +757,11 @@ def test_check_efi_efi_detected_non_intel(self): @unit_tests.mock(checks.system_info, "version", _gen_version(7, 9)) @unit_tests.mock(checks, "logger", GetLoggerMocked()) @unit_tests.mock(os.path, "exists", lambda x: x == "/usr/sbin/efibootmgr") - @unit_tests.mock(grub, "EFIBootInfo", EFIBootInfoMocked(exception=grub.BootloaderError("errmsg"))) + @unit_tests.mock( + grub, + "EFIBootInfo", + EFIBootInfoMocked(exception=grub.BootloaderError("errmsg")), + ) def test_check_efi_efi_detected_secure_boot(self): self._check_efi_critical( "The conversion with secure boot is currently not possible.\n" @@ -767,7 +775,11 @@ def test_check_efi_efi_detected_secure_boot(self): @unit_tests.mock(checks.system_info, "version", _gen_version(7, 9)) @unit_tests.mock(checks, "logger", GetLoggerMocked()) @unit_tests.mock(os.path, "exists", lambda x: x == "/usr/sbin/efibootmgr") - @unit_tests.mock(grub, "EFIBootInfo", EFIBootInfoMocked(exception=grub.BootloaderError("errmsg"))) + @unit_tests.mock( + grub, + "EFIBootInfo", + EFIBootInfoMocked(exception=grub.BootloaderError("errmsg")), + ) def test_check_efi_efi_detected_bootloader_error(self): self._check_efi_critical("errmsg") @@ -986,7 +998,11 @@ def __call__(self, command, *args, **kwargs): return self.return_string, self.return_code @unit_tests.mock(system_info, "version", namedtuple("Version", ["major", "minor"])(7, 0)) - @unit_tests.mock(checks, "call_yum_cmd", CallYumCmdMocked(ret_code=0, ret_string="Abcdef")) + @unit_tests.mock( + checks, + "call_yum_cmd", + CallYumCmdMocked(ret_code=0, ret_string="Abcdef"), + ) @unit_tests.mock(checks, "logger", GetLoggerMocked()) @unit_tests.mock(tool_opts, "no_rhsm", True) def test_custom_repos_are_valid(self): @@ -998,7 +1014,11 @@ def test_custom_repos_are_valid(self): ) @unit_tests.mock(system_info, "version", namedtuple("Version", ["major", "minor"])(7, 0)) - @unit_tests.mock(checks, "call_yum_cmd", CallYumCmdMocked(ret_code=1, ret_string="Abcdef")) + @unit_tests.mock( + checks, + "call_yum_cmd", + CallYumCmdMocked(ret_code=1, ret_string="Abcdef"), + ) @unit_tests.mock(checks, "logger", GetLoggerMocked()) @unit_tests.mock(tool_opts, "no_rhsm", True) def test_custom_repos_are_invalid(self): diff --git a/convert2rhel/unit_tests/main_test.py b/convert2rhel/unit_tests/main_test.py index 20befc0e7e..5c18bb1e06 100644 --- a/convert2rhel/unit_tests/main_test.py +++ b/convert2rhel/unit_tests/main_test.py @@ -17,7 +17,6 @@ import os -import sys import unittest from collections import OrderedDict @@ -181,7 +180,7 @@ def test_rollback_changes(self): @unit_tests.mock(cert.SystemCert, "_get_cert", lambda _get_cert: ("anything", "anything")) @mock_calls(main.special_cases, "check_and_resolve", CallOrderMocked) @mock_calls(pkghandler, "install_gpg_keys", CallOrderMocked) - @mock_calls(main.checks, "perform_pre_checks", CallOrderMocked) + @mock_calls(main.checks, "perform_system_checks", CallOrderMocked) @mock_calls(main.checks, "perform_pre_ponr_checks", CallOrderMocked) @mock_calls(pkghandler, "remove_excluded_pkgs", CallOrderMocked) @mock_calls(subscription, "replace_subscription_manager", CallOrderMocked) @@ -217,7 +216,7 @@ def test_pre_ponr_conversion_order_with_rhsm(self): intended_call_order["remove_repofile_pkgs"] = 1 intended_call_order["enable_repos"] = 1 intended_call_order["perform_pre_ponr_checks"] = 1 - intended_call_order["perform_pre_checks"] = 1 + intended_call_order["perform_system_checks"] = 1 # Merge the two together like a zipper, creates a tuple which we can assert with - including method call order! zipped_call_order = zip(intended_call_order.items(), self.CallOrderMocked.calls.items()) @@ -230,7 +229,7 @@ def test_pre_ponr_conversion_order_with_rhsm(self): @unit_tests.mock(cert.SystemCert, "_get_cert", lambda _get_cert: ("anything", "anything")) @mock_calls(main.special_cases, "check_and_resolve", CallOrderMocked) @mock_calls(pkghandler, "install_gpg_keys", CallOrderMocked) - @mock_calls(main.checks, "perform_pre_checks", CallOrderMocked) + @mock_calls(main.checks, "perform_system_checks", CallOrderMocked) @mock_calls(main.checks, "perform_pre_ponr_checks", CallOrderMocked) @mock_calls(pkghandler, "remove_excluded_pkgs", CallOrderMocked) @mock_calls(subscription, "replace_subscription_manager", CallOrderMocked) @@ -270,6 +269,59 @@ def test_pre_ponr_conversion_order_without_rhsm(self): intended_call_order["remove_repofile_pkgs"] = 1 intended_call_order["enable_repos"] = 0 + intended_call_order["perform_pre_ponr_checks"] = 1 + intended_call_order["perform_system_checks"] = 1 + + # Merge the two together like a zipper, creates a tuple which we can assert with - including method call order! + zipped_call_order = zip(intended_call_order.items(), self.CallOrderMocked.calls.items()) + for expected, actual in zipped_call_order: + if expected[1] > 0: + self.assertEqual(expected, actual) + + @unit_tests.mock(main.logging, "getLogger", GetLoggerMocked()) + @unit_tests.mock(tool_opts, "no_rhsm", False) + @unit_tests.mock(cert.SystemCert, "_get_cert", lambda _get_cert: ("anything", "anything")) + @mock_calls(main.special_cases, "check_and_resolve", CallOrderMocked) + @mock_calls(pkghandler, "install_gpg_keys", CallOrderMocked) + @mock_calls(main.checks, "perform_pre_ponr_checks", CallOrderMocked) + @mock_calls(pkghandler, "remove_excluded_pkgs", CallOrderMocked) + @mock_calls(subscription, "replace_subscription_manager", CallOrderMocked) + @mock_calls(subscription, "verify_rhsm_installed", CallOrderMocked) + @mock_calls(pkghandler, "remove_repofile_pkgs", CallOrderMocked) + @mock_calls(cert.SystemCert, "install", CallOrderMocked) + @mock_calls(pkghandler, "list_third_party_pkgs", CallOrderMocked) + @mock_calls(subscription, "subscribe_system", CallOrderMocked) + @mock_calls(repo, "get_rhel_repoids", CallOrderMocked) + @mock_calls(subscription, "check_needed_repos_availability", CallOrderMocked) + @mock_calls(subscription, "disable_repos", CallOrderMocked) + @mock_calls(subscription, "enable_repos", CallOrderMocked) + @mock_calls(subscription, "download_rhsm_pkgs", CallOrderMocked) + @unit_tests.mock(checks, "check_readonly_mounts", GetFakeFunctionMocked) + def test_pre_ponr_conversion_order_without_rhsm(self): + self.CallOrderMocked.reset() + main.pre_ponr_conversion() + + intended_call_order = OrderedDict() + + intended_call_order["list_third_party_pkgs"] = 1 + intended_call_order["remove_excluded_pkgs"] = 1 + intended_call_order["check_and_resolve"] = 1 + intended_call_order["install_gpg_keys"] = 1 + + # Do not expect this one to be called - related to RHSM + intended_call_order["download_rhsm_pkgs"] = 0 + intended_call_order["replace_subscription_manager"] = 0 + intended_call_order["verify_rhsm_installed"] = 0 + intended_call_order["install"] = 0 + intended_call_order["subscribe_system"] = 0 + intended_call_order["get_rhel_repoids"] = 0 + intended_call_order["check_needed_repos_availability"] = 0 + intended_call_order["disable_repos"] = 0 + + intended_call_order["remove_repofile_pkgs"] = 1 + + intended_call_order["enable_repos"] = 0 + intended_call_order["perform_pre_ponr_checks"] = 1 # Merge the two together like a zipper, creates a tuple which we can assert with - including method call order! @@ -340,12 +392,12 @@ def test_main(monkeypatch): resolve_system_info_mock = mock.Mock() collect_early_data_mock = mock.Mock() clean_yum_metadata_mock = mock.Mock() - perform_pre_checks_mock = mock.Mock() system_release_file_mock = mock.Mock() os_release_file_mock = mock.Mock() backup_varsdir_mock = mock.Mock() backup_yum_repos_mock = mock.Mock() clear_versionlock_mock = mock.Mock() + perform_system_checks_mock = mock.Mock() pre_ponr_conversion_mock = mock.Mock() ask_to_continue_mock = mock.Mock() post_ponr_conversion_mock = mock.Mock() @@ -364,11 +416,11 @@ def test_main(monkeypatch): monkeypatch.setattr(breadcrumbs, "collect_early_data", collect_early_data_mock) monkeypatch.setattr(pkghandler, "clear_versionlock", clear_versionlock_mock) monkeypatch.setattr(pkghandler, "clean_yum_metadata", clean_yum_metadata_mock) - monkeypatch.setattr(checks, "perform_pre_checks", perform_pre_checks_mock) monkeypatch.setattr(system_release_file, "backup", system_release_file_mock) monkeypatch.setattr(os_release_file, "backup", os_release_file_mock) monkeypatch.setattr(repo, "backup_yum_repos", backup_yum_repos_mock) monkeypatch.setattr(repo, "backup_varsdir", backup_varsdir_mock) + monkeypatch.setattr(checks, "perform_system_checks", perform_system_checks_mock) monkeypatch.setattr(main, "pre_ponr_conversion", pre_ponr_conversion_mock) monkeypatch.setattr(utils, "ask_to_continue", ask_to_continue_mock) monkeypatch.setattr(main, "post_ponr_conversion", post_ponr_conversion_mock) @@ -387,7 +439,6 @@ def test_main(monkeypatch): assert resolve_system_info_mock.call_count == 1 assert collect_early_data_mock.call_count == 1 assert clean_yum_metadata_mock.call_count == 1 - assert perform_pre_checks_mock.call_count == 1 assert system_release_file_mock.call_count == 1 assert os_release_file_mock.call_count == 1 assert backup_yum_repos_mock.call_count == 1 @@ -435,7 +486,7 @@ def test_main_rollback_pre_ponr_changes_phase(monkeypatch): resolve_system_info_mock = mock.Mock() collect_early_data_mock = mock.Mock() clean_yum_metadata_mock = mock.Mock() - perform_pre_checks_mock = mock.Mock() + perform_system_checks_mock = mock.Mock() system_release_file_mock = mock.Mock() os_release_file_mock = mock.Mock() backup_yum_repos_mock = mock.Mock() @@ -455,7 +506,7 @@ def test_main_rollback_pre_ponr_changes_phase(monkeypatch): monkeypatch.setattr(breadcrumbs, "collect_early_data", collect_early_data_mock) monkeypatch.setattr(pkghandler, "clear_versionlock", clear_versionlock_mock) monkeypatch.setattr(pkghandler, "clean_yum_metadata", clean_yum_metadata_mock) - monkeypatch.setattr(checks, "perform_pre_checks", perform_pre_checks_mock) + monkeypatch.setattr(checks, "perform_system_checks", perform_system_checks_mock) monkeypatch.setattr(system_release_file, "backup", system_release_file_mock) monkeypatch.setattr(os_release_file, "backup", os_release_file_mock) monkeypatch.setattr(repo, "backup_yum_repos", backup_yum_repos_mock) @@ -472,7 +523,7 @@ def test_main_rollback_pre_ponr_changes_phase(monkeypatch): assert resolve_system_info_mock.call_count == 1 assert collect_early_data_mock.call_count == 1 assert clean_yum_metadata_mock.call_count == 1 - assert perform_pre_checks_mock.call_count == 1 + assert perform_system_checks_mock.call_count == 1 assert system_release_file_mock.call_count == 1 assert os_release_file_mock.call_count == 1 assert backup_yum_repos_mock.call_count == 1 @@ -491,7 +542,7 @@ def test_main_rollback_post_ponr_changes_phase(monkeypatch, caplog): resolve_system_info_mock = mock.Mock() collect_early_data_mock = mock.Mock() clean_yum_metadata_mock = mock.Mock() - perform_pre_checks_mock = mock.Mock() + perform_system_checks_mock = mock.Mock() system_release_file_mock = mock.Mock() os_release_file_mock = mock.Mock() backup_yum_repos_mock = mock.Mock() @@ -513,7 +564,7 @@ def test_main_rollback_post_ponr_changes_phase(monkeypatch, caplog): monkeypatch.setattr(breadcrumbs, "collect_early_data", collect_early_data_mock) monkeypatch.setattr(pkghandler, "clear_versionlock", clear_versionlock_mock) monkeypatch.setattr(pkghandler, "clean_yum_metadata", clean_yum_metadata_mock) - monkeypatch.setattr(checks, "perform_pre_checks", perform_pre_checks_mock) + monkeypatch.setattr(checks, "perform_system_checks", perform_system_checks_mock) monkeypatch.setattr(system_release_file, "backup", system_release_file_mock) monkeypatch.setattr(os_release_file, "backup", os_release_file_mock) monkeypatch.setattr(repo, "backup_yum_repos", backup_yum_repos_mock) @@ -532,7 +583,7 @@ def test_main_rollback_post_ponr_changes_phase(monkeypatch, caplog): assert resolve_system_info_mock.call_count == 1 assert collect_early_data_mock.call_count == 1 assert clean_yum_metadata_mock.call_count == 1 - assert perform_pre_checks_mock.call_count == 1 + assert perform_system_checks_mock.call_count == 1 assert system_release_file_mock.call_count == 1 assert os_release_file_mock.call_count == 1 assert backup_yum_repos_mock.call_count == 1 diff --git a/convert2rhel/unit_tests/pkghandler_test.py b/convert2rhel/unit_tests/pkghandler_test.py index 9790049c06..1a894265ee 100644 --- a/convert2rhel/unit_tests/pkghandler_test.py +++ b/convert2rhel/unit_tests/pkghandler_test.py @@ -1935,3 +1935,37 @@ def test_get_system_packages_for_replacement(pretend_os, monkeypatch): result = pkghandler.get_system_packages_for_replacement() for pkg in pkgs: assert pkg in result + + +@pytest.mark.parametrize( + ("pkg_1", "pkg_2", "expected"), + ( + ( + "kernel-core-0:4.18.0-240.10.1.el8_3.x86_64", + "kernel-core-0:4.18.0-240.15.1.el8_3.x86_64", + -1, + ), + ( + "kmod-core-0:4.18.0-240.15.1.el8_3.x86_64", + "kmod-core-0:4.18.0-240.10.1.el8_3.x86_64", + 1, + ), + ( + "kmod-core-0:4.18.0-240.15.1.el8_3.x86_64", + "kmod-core-0:4.18.0-240.15.1.el8_3.x86_64", + 0, + ), + ( + "no-arch-0:4.18.0-240.15.1.el8_3", + "no-arch-0:4.18.0-240.15.1.el8_3", + 0, + ), + ( + "kmod-core-0.4.18.0-240.15.1.el8_3.x86_64", + "kmod-core-0.4.18.0-240.15.1.el8_3.x86_64", + 0, + ), + ), +) +def test__package_version_cmp(pkg_1, pkg_2, expected): + assert pkghandler._package_version_cmp(pkg_1, pkg_2) == expected diff --git a/convert2rhel/unit_tests/systeminfo_test.py b/convert2rhel/unit_tests/systeminfo_test.py index 6961025dbf..b877325120 100644 --- a/convert2rhel/unit_tests/systeminfo_test.py +++ b/convert2rhel/unit_tests/systeminfo_test.py @@ -15,7 +15,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -# Required imports: import logging import os import shutil @@ -247,10 +246,23 @@ def test_get_release_ver_other( ): monkeypatch.setattr(systeminfo.SystemInfo, "_get_cfg_opt", mock.Mock(return_value=releasever_val)) monkeypatch.setattr(systeminfo.SystemInfo, "_check_internet_access", mock.Mock(return_value=has_internet)) + monkeypatch.setattr( + systeminfo.SystemInfo, + "_get_cfg_opt", + mock.Mock(return_value=releasever_val), + ) if self_name: - monkeypatch.setattr(systeminfo.SystemInfo, "_get_system_name", mock.Mock(return_value=self_name)) + monkeypatch.setattr( + systeminfo.SystemInfo, + "_get_system_name", + mock.Mock(return_value=self_name), + ) if self_version: - monkeypatch.setattr(systeminfo.SystemInfo, "_get_system_version", mock.Mock(return_value=self_version)) + monkeypatch.setattr( + systeminfo.SystemInfo, + "_get_system_version", + mock.Mock(return_value=self_version), + ) # calling resolve_system_info one more time to enable our monkeypatches if exception: with pytest.raises(exception): @@ -389,3 +401,41 @@ def test_get_system_distribution_id(system_release_content, expected): @centos8 def test_get_system_distribution_id_default_system_release_content(pretend_os): assert system_info._get_system_distribution_id() == None + + +@pytest.mark.parametrize( + ( + "submgr_enabled_repos", + "tool_opts_no_rhsm", + "tool_opts_enablerepo", + "expected", + ), + ( + ( + ["rhel-repo1.repo", "rhel-repo2.repo"], + False, + [], + ["rhel-repo1.repo", "rhel-repo2.repo"], + ), + ( + ["rhel-repo1.repo", "rhel-repo2.repo"], + True, + ["cli-rhel-repo1.repo", "cli-rhel-repo2.repo"], + ["cli-rhel-repo1.repo", "cli-rhel-repo2.repo"], + ), + ), +) +def test_get_enabled_rhel_repos( + submgr_enabled_repos, + tool_opts_no_rhsm, + tool_opts_enablerepo, + expected, + global_tool_opts, + monkeypatch, +): + monkeypatch.setattr(systeminfo, "tool_opts", global_tool_opts) + system_info.submgr_enabled_repos = submgr_enabled_repos + global_tool_opts.enablerepo = tool_opts_enablerepo + global_tool_opts.no_rhsm = tool_opts_no_rhsm + + assert system_info.get_enabled_rhel_repos() == expected diff --git a/convert2rhel/unit_tests/toolopts_test.py b/convert2rhel/unit_tests/toolopts_test.py index 676557e762..4872a2cae5 100644 --- a/convert2rhel/unit_tests/toolopts_test.py +++ b/convert2rhel/unit_tests/toolopts_test.py @@ -20,7 +20,6 @@ import os import sys -import unittest from collections import namedtuple @@ -30,7 +29,6 @@ import convert2rhel.toolopts import convert2rhel.utils -from convert2rhel import unit_tests # Imports unit_tests/__init__.py from convert2rhel.toolopts import tool_opts diff --git a/tests/integration/tier0/inhibit-if-kmods-is-not-supported/main.fmf b/tests/integration/tier0/inhibit-if-kmods-is-not-supported/main.fmf index 6c71abc431..83ac071dfa 100644 --- a/tests/integration/tier0/inhibit-if-kmods-is-not-supported/main.fmf +++ b/tests/integration/tier0/inhibit-if-kmods-is-not-supported/main.fmf @@ -1,6 +1,11 @@ -summary: kmods tests +summary: Handle custom, tainted and force loaded kernel modules. +description: | + Verify, that Convert2RHEL is able to handle custom, tainted and force loaded kernel modules. tier: 0 +tag+: + - kernel + test: | pytest -svv diff --git a/tests/integration/tier0/inhibit-if-kmods-is-not-supported/test_kmods_not_supported.py b/tests/integration/tier0/inhibit-if-kmods-is-not-supported/test_kmods_not_supported.py index 498ac9c0af..a16ee11c8f 100644 --- a/tests/integration/tier0/inhibit-if-kmods-is-not-supported/test_kmods_not_supported.py +++ b/tests/integration/tier0/inhibit-if-kmods-is-not-supported/test_kmods_not_supported.py @@ -7,8 +7,16 @@ from envparse import env +system_version = platform.platform() + + @pytest.fixture() def insert_custom_kmod(shell): + """ + Move the kernel module to custom location, so it is marked as custom. + Then load the kernel module. + """ + def factory(): origin_kmod_loc = Path("/lib/modules/$(uname -r)/kernel/drivers/net/bonding/bonding.ko.xz") new_kmod_dir = origin_kmod_loc.parent / "custom_module_location" @@ -22,25 +30,31 @@ def factory(): def test_inhibit_if_custom_module_loaded(insert_custom_kmod, convert2rhel): - # This test checks that rpmquery works correctly. - # If custom module is loaded the conversion has to be inhibited. + """ + This test verifies that rpmquery for detecting supported kernel modules in RHEL works correctly. + If custom module is loaded the conversion has to be inhibited. + """ insert_custom_kmod() with convert2rhel( - ("-y --no-rpm-va --serverurl {} --username {} --password {} --pool {} --debug").format( + "-y --no-rpm-va --serverurl {} --username {} --password {} --pool {} --debug".format( env.str("RHSM_SERVER_URL"), env.str("RHSM_USERNAME"), env.str("RHSM_PASSWORD"), env.str("RHSM_POOL"), ) ) as c2r: - c2r.expect("The following kernel modules are not supported in RHEL") + c2r.expect("The following loaded kernel modules are not available in RHEL") assert c2r.exitstatus != 0 def test_do_not_inhibit_if_module_is_not_loaded(shell, convert2rhel): + """ + Test removes previously loaded custom module and runs the conversion. + The kmod compatibility checks is right before the point of no return. + Abort the conversion right after the check. + """ assert shell("modprobe -r -v bonding").returncode == 0 - system_version = platform.platform() if "oracle-7" in system_version or "centos-7" in system_version: prompt_amount = 3 elif "oracle-8" in system_version: @@ -49,7 +63,7 @@ def test_do_not_inhibit_if_module_is_not_loaded(shell, convert2rhel): prompt_amount = 3 # If custom module is not loaded the conversion is not inhibited. with convert2rhel( - ("--no-rpm-va --serverurl {} --username {} --password {} --pool {} --debug").format( + "--no-rpm-va --serverurl {} --username {} --password {} --pool {} --debug".format( env.str("RHSM_SERVER_URL"), env.str("RHSM_USERNAME"), env.str("RHSM_PASSWORD"), @@ -61,16 +75,44 @@ def test_do_not_inhibit_if_module_is_not_loaded(shell, convert2rhel): c2r.sendline("y") prompt_amount -= 1 - assert c2r.expect("Kernel modules are compatible.", timeout=600) == 0 + # Stop conversion before the point of no return as we do not need to run the full conversion + # with the check for kompatible kernel modules being right before the PONR. + assert c2r.expect("All loaded kernel modules are available in RHEL.", timeout=600) == 0 c2r.expect("Continue with the system conversion?") c2r.sendline("n") assert c2r.exitstatus != 0 -def test_tainted_kernel_inhibitor(shell, convert2rhel): - # This test marks the kernel as tainted which is not supported by convert2rhel. +def test_do_not_inhibit_if_module_is_force_loaded(shell, convert2rhel): + """ + Test force loads kmod and verifies that Convert2RHEL run is being inhibited. + Force loaded kmods are denoted (FE) where F = module was force loaded E = unsigned module was loaded. + Convert2RHEL sees force loaded kmod as tainted. + """ + if "oracle-7" not in system_version and "centos-7" not in system_version: + # Force load the kernel module + assert shell("modprobe -f -v bonding").returncode == 0 + # Check for force loaded modules being flagged FE in /proc/modules + assert "(FE)" in shell("cat /proc/modules").output - # We need to install specific kernel packages to build own custom kernel module. + with convert2rhel("--no-rpm-va --debug") as c2r: + assert c2r.expect("Tainted kernel modules detected") == 0 + assert c2r.exitstatus != 0 + + # Clean up - unload kmod and check for force loaded modules not being in /proc/modules + assert shell("modprobe -r -v bonding").returncode == 0 + assert "(FE)" not in shell("cat /proc/modules").output + + +def test_tainted_kernel_inhibitor(shell, convert2rhel): + """ + This test marks the kernel as tainted which is not supported by Convert2RHEL. + We need to install specific kernel packages to build own custom kernel module. + Build own kmod from source file that has been copied to the testing machine during preparation phase. + This kmod marks the system with the P, O and E flags. + """ + + # Install kernel packages shell("yum -y install gcc make kernel-headers kernel-devel-$(uname -r) elfutils-libelf-devel") # Build own kmod form source file that has been copied to the testing machine during preparation phase. @@ -79,12 +121,12 @@ def test_tainted_kernel_inhibitor(shell, convert2rhel): assert shell("insmod /tmp/my-test/my_kmod.ko").returncode == 0 with convert2rhel( - ("-y --no-rpm-va --serverurl {} --username {} --password {} --pool {} --debug").format( + "-y --no-rpm-va --serverurl {} --username {} --password {} --pool {} --debug".format( env.str("RHSM_SERVER_URL"), env.str("RHSM_USERNAME"), env.str("RHSM_PASSWORD"), env.str("RHSM_POOL"), ) ) as c2r: - c2r.expect("Tainted kernel module\(s\) detected") + c2r.expect("Tainted kernel modules detected") assert c2r.exitstatus != 0