diff --git a/.devcontainer/alma9/devcontainer.json b/.devcontainer/alma9/devcontainer.json index d0e8e628d9..28884a2d2d 100644 --- a/.devcontainer/alma9/devcontainer.json +++ b/.devcontainer/alma9/devcontainer.json @@ -31,8 +31,8 @@ "customizations": { "vscode": { "settings": { - "extensions.autoUpdate": false, - "extensions.autoCheckUpdates": false, + "extensions.autoUpdate": true, + "extensions.autoCheckUpdates": true, "python.languageServer": "Pylance" }, "extensions": [ @@ -41,7 +41,7 @@ "GitHub.vscode-pull-request-github", "Cameron.vscode-pytest", "njpwerner.autodocstring", - "ms-python.python@2022.8.1" + "ms-python.python" ] } } diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 00ad628993..123a1a7862 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,18 +9,14 @@ repos: language: python language_version: "python3" additional_dependencies: [click==7.1.2, black==21.12b0] - - repo: local + - repo: https://github.com/pylint-dev/pylint + rev: v3.1.0 hooks: - id: pylint - name: pylint - entry: pylint - types: [python] - language: python - additional_dependencies: [pylint==2.15.10] args: [ - "-sn", # Don't display the score - "--rcfile=.pylintrc", # Link to your config file - ] + "-sn", # Don't display the score + "--rcfile=.pylintrc", # Link to your config file + ] - repo: "https://github.com/timothycrosley/isort" rev: 5.13.2 hooks: diff --git a/.pylintrc b/.pylintrc index 2a8cc828d7..9d53a5461d 100644 --- a/.pylintrc +++ b/.pylintrc @@ -15,6 +15,7 @@ disable= # on the versions of python we support. redundant-u-string-prefix, # Python 3.0+ consider-using-f-string, # Python 3.6+ + use-yield-from, # Python 3+ # These are our current failures 2023-01-11. We can go through them and either # fix them or add a comment to say why we are leaving it disabled. anomalous-backslash-in-string, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3e2f92f059..5aa0e0349e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,12 +76,6 @@ Dev Containers: Open Folder in Container You are now inside a container as indicated by the bottom left box. There is an extension for tests in the left navigation that auto-discovers tests that you can run. This will be much faster than developing using `make tests` -#### Known issues -##### Debugging just loads and does nothing -Versions newer than `2022.8.1` of extension `ms-python.python` will not work when debugging code. We have pinned the version but VS Code might auto-update the extension. - -To work around this, go to extensions tab and select Ignore Updates on the extension, thereafter select install a new version and install `2022.8.1`. It will prompt to reload the window, make sure the right version is installed after reload. Now debugging should work - ### Dependencies for local development We have some required dependencies you should have installed on your system diff --git a/convert2rhel/actions/pre_ponr_changes/handle_packages.py b/convert2rhel/actions/pre_ponr_changes/handle_packages.py index dae409cb42..df47a0abff 100644 --- a/convert2rhel/actions/pre_ponr_changes/handle_packages.py +++ b/convert2rhel/actions/pre_ponr_changes/handle_packages.py @@ -19,10 +19,11 @@ import logging import os -from convert2rhel import actions, pkghandler +from convert2rhel import actions, pkghandler, utils +from convert2rhel.backup import BACKUP_DIR, backup_control +from convert2rhel.backup.packages import RestorablePackage from convert2rhel.repo import DEFAULT_YUM_REPOFILE_DIR from convert2rhel.systeminfo import system_info -from convert2rhel.utils import BACKUP_DIR logger = logging.getLogger(__name__) @@ -65,149 +66,122 @@ def extract_packages(self, pkg): return pkg.nevra.name -class RemoveExcludedPackages(actions.Action): - id = "REMOVE_EXCLUDED_PACKAGES" +class RemoveSpecialPackages(actions.Action): + id = "REMOVE_SPECIAL_PACKAGES" dependencies = ( + # We use the backed up repos in remove_pkgs_unless_from_redhat() "BACKUP_REPOSITORY", "BACKUP_PACKAGE_FILES", - ) # We use the backed up repos in remove_pkgs_unless_from_redhat() + "BACKUP_REDHAT_RELEASE", + ) def run(self): + """Remove a set of special packages from the system. + + The packages marked for exclusion here are the excluded_pkgs and + repofile_pkgs that comes from the system_info singleton. This class + substitute the old RemoveExcludedPackages and RemoveRepofilePackages as + both of them depends on the RestorablePackage class, in which case, the + RestorablePackage was designed to handle a set of packages in the + moment of instantiation of the class, but since both removal classes + executed separately, we couldn't properly reinstall some packages that + got excluded and backed up, as they had to be reinstalled in the system + in the same RPM transaction call. To not redo how the RestorablePackage + works, both RemoveExcludedPackages and RemoveRepofilePackages classes + got merged together into this one, making possible to remove and back + up all the packages in a single transaction. """ - Certain packages need to be removed before the system conversion, - depending on the system to be converted. - """ - super(RemoveExcludedPackages, self).run() - - logger.task("Convert: Remove excluded packages") - logger.info("Searching for the following excluded packages:\n") - + all_pkgs = [] pkgs_removed = [] - # Since the MD5 checksum of original path is used in backup path to avoid - # conflicts in backup folder, preparing the path is needed. - backedup_reposdir = os.path.join(BACKUP_DIR, hashlib.md5(DEFAULT_YUM_REPOFILE_DIR.encode()).hexdigest()) - try: - pkgs_to_remove = sorted(pkghandler.get_packages_to_remove(system_info.excluded_pkgs)) - # this call can return None, which is not ideal to use with sorted. - pkgs_removed = sorted(pkghandler.remove_pkgs_unless_from_redhat(pkgs_to_remove, backedup_reposdir) or []) + logger.task("Convert: Searching for the following excluded packages") + excluded_pkgs = sorted(pkghandler.get_packages_to_remove(system_info.excluded_pkgs)) - # TODO: Handling SystemExit here as way to speedup exception - # handling and not refactor contents of the underlying function. + logger.task( + "Convert: Searching for packages containing .repo files or affecting variables in the .repo files" + ) + repofile_pkgs = sorted(pkghandler.get_packages_to_remove(system_info.repofile_pkgs)) + + logger.info("\n") + + all_pkgs = excluded_pkgs + repofile_pkgs + if not all_pkgs: + logger.info("No packages to backup and remove.") + return + + # We're using the backed up yum repositories to prevent the following: + # - the system was registered to RHSM prior to the conversion and the system didn't have the redhat.repo generated + # for the lack of the RHSM product certificate + # - at this point convert2rhel has installed the RHSM product cert (e.g. /etc/pki/product-default/69.pem) + # - this function might be performing the first yum call convert2rhel does after cleaning yum metadata + # - the "subscription-manager" yum plugin spots that there's a new RHSM product cert and generates + # /etc/yum.repos.d/redhat.repo + # - the suddenly enabled RHEL repos cause a package backup failure + # Since the MD5 checksum of original path is used in backup path to avoid + # conflicts in backup folder, preparing the path is needed. + backedup_reposdir = os.path.join(BACKUP_DIR, hashlib.md5(DEFAULT_YUM_REPOFILE_DIR.encode()).hexdigest()) + backup_control.push(RestorablePackage(pkgs=pkghandler.get_pkg_nevras(all_pkgs), reposdir=backedup_reposdir)) + + logger.info("\nRemoving special packages from the system.") + pkgs_removed = _remove_packages_unless_from_redhat(pkgs_list=all_pkgs) except SystemExit as e: # TODO(r0x0d): Places where we raise SystemExit and need to be # changed to something more specific. # - When we can't remove a package. self.set_result( level="ERROR", - id="EXCLUDED_PACKAGE_REMOVAL_FAILED", - title="Failed to remove excluded package", + id="SPECIAL_PACKAGE_REMOVAL_FAILED", + title="Failed to remove some packages necessary for the conversion.", description="The cause of this error is unknown, please look at the diagnosis for more information.", diagnosis=str(e), ) return # shows which packages were not removed, if false, all packages were removed - pkgs_not_removed = sorted(frozenset(pkghandler.get_pkg_nevras(pkgs_to_remove)).difference(pkgs_removed)) + pkgs_not_removed = sorted(frozenset(pkghandler.get_pkg_nevras(all_pkgs)).difference(pkgs_removed)) if pkgs_not_removed: message = "The following packages were not removed: %s" % ", ".join(pkgs_not_removed) logger.warning(message) self.add_message( level="WARNING", - id="EXCLUDED_PACKAGES_NOT_REMOVED", - title="Excluded packages not removed", - description="Excluded packages which could not be removed", + id="SPECIAL_PACKAGES_NOT_REMOVED", + title="Special packages not removed", + description="Special packages which could not be removed", diagnosis=message, ) + if pkgs_removed: message = "The following packages will be removed during the conversion: %s" % ", ".join(pkgs_removed) logger.info(message) self.add_message( level="INFO", - id="EXCLUDED_PACKAGES_REMOVED", - title="Excluded packages to be removed", - description="We have identified installed packages that match a pre-defined list of packages that are" - " to be removed during the conversion", + id="SPECIAL_PACKAGES_REMOVED", + title="Special packages to be removed", + description=( + "We have identified installed packages that match a pre-defined list of packages that are" + " to be removed during the conversion" + ), diagnosis=message, ) + super(RemoveSpecialPackages, self).run() -class RemoveRepositoryFilesPackages(actions.Action): - id = "REMOVE_REPOSITORY_FILES_PACKAGES" - dependencies = ( - "BACKUP_REDHAT_RELEASE", - # We use the backed up repos in remove_pkgs_unless_from_redhat() - "BACKUP_REPOSITORY", - # The installation of sub-man pkgs needs access to the original repofiles to get the sub-man deps from there - "PRE_SUBSCRIPTION", - "BACKUP_PACKAGE_FILES", - ) - def run(self): - """ - Remove those non-RHEL packages that contain YUM/DNF repofiles - (/etc/yum.repos.d/*.repo) or affect variables in the repofiles (e.g. - $releasever). - - Red Hat cannot automatically remove these non-RHEL packages with other - excluded packages. While other excluded packages must be removed before - installing subscription-manager to prevent package conflicts, these - non-RHEL packages must be present on the system during - subscription-manager installation so that the system can access and - install subscription-manager dependencies. As a result, these non-RHEL - packages must be manually removed after subscription-manager - installation. - """ - super(RemoveRepositoryFilesPackages, self).run() +def _remove_packages_unless_from_redhat(pkgs_list): + """Remove packages from the system that are not RHEL. - logger.task("Convert: Remove packages containing .repo files") - logger.info("Searching for packages containing .repo files or affecting variables in the .repo files:\n") + :param pkgs_list list[str]: Packages that will be removed. + :return list[str]: Packages removed from the system. + """ + if not pkgs_list: + logger.info("\nNothing to do.") + return [] - pkgs_removed = [] - # Since the MD5 checksum of original path is used in backup path to avoid - # conflicts in backup folder, preparing the path is needed. - backedup_reposdir = os.path.join(BACKUP_DIR, hashlib.md5(DEFAULT_YUM_REPOFILE_DIR.encode()).hexdigest()) - - try: - pkgs_to_remove = sorted(pkghandler.get_packages_to_remove(system_info.repofile_pkgs)) - # this call can return None, which is not ideal to use with sorted. - pkgs_removed = sorted(pkghandler.remove_pkgs_unless_from_redhat(pkgs_to_remove, backedup_reposdir) or []) + # this call can return None, which is not ideal to use with sorted. + logger.warning("Removing the following %s packages:\n" % len(pkgs_list)) + pkghandler.print_pkg_info(pkgs_list) - # TODO: Handling SystemExit here as way to speedup exception - # handling and not refactor contents of the underlying function. - except SystemExit as e: - # TODO(r0x0d): Places where we raise SystemExit and need to be - # changed to something more specific. - # - When we can't remove a package. - self.set_result( - level="ERROR", - id="REPOSITORY_FILE_PACKAGE_REMOVAL_FAILED", - title="Repository file package removal failure", - description="The cause of this error is unknown, please look at the diagnosis for more information.", - diagnosis=str(e), - ) - return + pkgs_removed = utils.remove_pkgs(pkghandler.get_pkg_nevras(pkgs_list)) + logger.debug("Successfully removed %s packages" % len(pkgs_list)) - # shows which packages were not removed, if false, all packages were removed - pkgs_not_removed = sorted(frozenset(pkghandler.get_pkg_nevras(pkgs_to_remove)).difference(pkgs_removed)) - if pkgs_not_removed: - message = "The following packages were not removed: %s" % ", ".join(pkgs_not_removed) - logger.warning(message) - self.add_message( - level="WARNING", - id="REPOSITORY_FILE_PACKAGES_NOT_REMOVED", - title="Repository file packages not removed", - description="Repository file packages which could not be removed", - diagnosis=message, - ) - if pkgs_removed: - message = "The following packages will be removed during the conversion: %s" % ", ".join(pkgs_removed) - logger.info(message) - self.add_message( - level="INFO", - id="REPOSITORY_FILE_PACKAGES_REMOVED", - title="Repository file packages to be removed", - description="We have identified installed packages that match a pre-defined list of packages that are" - " to be removed during the conversion", - diagnosis=message, - ) + return pkgs_removed diff --git a/convert2rhel/actions/pre_ponr_changes/subscription.py b/convert2rhel/actions/pre_ponr_changes/subscription.py index 944308b71c..29526834c7 100644 --- a/convert2rhel/actions/pre_ponr_changes/subscription.py +++ b/convert2rhel/actions/pre_ponr_changes/subscription.py @@ -68,9 +68,9 @@ def run(self): class PreSubscription(actions.Action): id = "PRE_SUBSCRIPTION" dependencies = ( + "REMOVE_SPECIAL_PACKAGES", "INSTALL_RED_HAT_CERT_FOR_YUM", "INSTALL_RED_HAT_GPG_KEY", - "REMOVE_EXCLUDED_PACKAGES", ) def run(self): @@ -159,7 +159,6 @@ class SubscribeSystem(actions.Action): id = "SUBSCRIBE_SYSTEM" dependencies = ( # Implicit dependency for `BACKUP_REDHAT_RELEASE` - "REMOVE_REPOSITORY_FILES_PACKAGES", "PRE_SUBSCRIPTION", "EUS_SYSTEM_CHECK", ) diff --git a/convert2rhel/actions/pre_ponr_changes/transaction.py b/convert2rhel/actions/pre_ponr_changes/transaction.py index ad0f48ecc0..d5dce2eedd 100644 --- a/convert2rhel/actions/pre_ponr_changes/transaction.py +++ b/convert2rhel/actions/pre_ponr_changes/transaction.py @@ -27,7 +27,6 @@ class ValidatePackageManagerTransaction(actions.Action): id = "VALIDATE_PACKAGE_MANAGER_TRANSACTION" dependencies = ( "INSTALL_RED_HAT_GPG_KEY", - "REMOVE_EXCLUDED_PACKAGES", # This package can cause problems during the validation. Since no one # is depending on this action, it may run whenever it wants to, which # can cause problems. diff --git a/convert2rhel/actions/system_checks/custom_repos_are_valid.py b/convert2rhel/actions/system_checks/custom_repos_are_valid.py index 3ff50b037e..4598ffbc54 100644 --- a/convert2rhel/actions/system_checks/custom_repos_are_valid.py +++ b/convert2rhel/actions/system_checks/custom_repos_are_valid.py @@ -18,7 +18,7 @@ import logging from convert2rhel import actions -from convert2rhel.pkghandler import call_yum_cmd +from convert2rhel.pkgmanager import call_yum_cmd from convert2rhel.toolopts import tool_opts diff --git a/convert2rhel/actions/system_checks/rhel_compatible_kernel.py b/convert2rhel/actions/system_checks/rhel_compatible_kernel.py index 846e30c1e0..feb7e6ea38 100644 --- a/convert2rhel/actions/system_checks/rhel_compatible_kernel.py +++ b/convert2rhel/actions/system_checks/rhel_compatible_kernel.py @@ -99,7 +99,7 @@ def _bad_kernel_version(kernel_release): raise KernelIncompatibleError( "UNEXPECTED_VERSION", "Unexpected OS major version. Expected: {compatible_version}", - dict(compatible_version=COMPATIBLE_KERNELS_VERS.keys()), + {"compatible_version": COMPATIBLE_KERNELS_VERS.keys()}, ) if incompatible_version: @@ -107,11 +107,11 @@ def _bad_kernel_version(kernel_release): "INCOMPATIBLE_VERSION", "Booted kernel version '{kernel_version}' does not correspond to the version " "'{compatible_version}' available in RHEL {rhel_major_version}", - dict( - kernel_version=kernel_version, - compatible_version=COMPATIBLE_KERNELS_VERS[system_info.version.major], - rhel_major_version=system_info.version.major, - ), + { + "kernel_version": kernel_version, + "compatible_version": COMPATIBLE_KERNELS_VERS[system_info.version.major], + "rhel_major_version": system_info.version.major, + }, ) logger.debug( @@ -134,7 +134,7 @@ def _bad_kernel_package_signature(kernel_release): "UNSIGNED_PACKAGE", "The booted kernel {vmlinuz_path} is not owned by any installed package." " It needs to be owned by a package signed by {os_vendor}.", - dict(vmlinuz_path=vmlinuz_path, os_vendor=os_vendor), + {"vmlinuz_path": vmlinuz_path, "os_vendor": os_vendor}, ) kernel_pkg_obj = get_installed_pkg_information(pkg_name=kernel_pkg) @@ -143,7 +143,7 @@ def _bad_kernel_package_signature(kernel_release): raise KernelIncompatibleError( "INVALID_KERNEL_PACKAGE_SIGNATURE", "Custom kernel detected. The booted kernel needs to be signed by {os_vendor}.", - dict(os_vendor=os_vendor), + {"os_vendor": os_vendor}, ) logger.debug("The booted kernel is signed by %s." % os_vendor) @@ -158,6 +158,6 @@ def _bad_kernel_substring(kernel_release): "INVALID_PACKAGE_SUBSTRING", "The booted kernel '{kernel_release}' contains one of the disallowed " "substrings: {bad_kernel_release_substrings}", - dict(kernel_release=kernel_release, bad_kernel_release_substrings=BAD_KERNEL_RELEASE_SUBSTRINGS), + {"kernel_release": kernel_release, "bad_kernel_release_substrings": BAD_KERNEL_RELEASE_SUBSTRINGS}, ) return False diff --git a/convert2rhel/backup/__init__.py b/convert2rhel/backup/__init__.py index 0ceb351b52..e9e3851b0b 100644 --- a/convert2rhel/backup/__init__.py +++ b/convert2rhel/backup/__init__.py @@ -20,113 +20,16 @@ import abc import logging import os -import re import six -from convert2rhel import exceptions, utils -from convert2rhel.repo import get_hardcoded_repofiles_dir -from convert2rhel.systeminfo import system_info -from convert2rhel.utils import BACKUP_DIR, download_pkg, remove_orphan_folders, run_subprocess +from convert2rhel.utils import TMP_DIR -loggerinst = logging.getLogger(__name__) - -# Note: Currently the only use case for this is package removals -class ChangedRPMPackagesController: - """Keep control of installed/removed RPM pkgs for backup/restore.""" +# Directory for temporary backing up files, packages and other relevant stuff. +BACKUP_DIR = os.path.join(TMP_DIR, "backup") - def __init__(self): - self.installed_pkgs = [] - self.removed_pkgs = [] - - def track_installed_pkg(self, pkg): - """Add a installed RPM pkg to the list of installed pkgs.""" - self.installed_pkgs.append(pkg) - - def track_installed_pkgs(self, pkgs): - """Track packages installed before the PONR to be able to remove them later (roll them back) if needed.""" - self.installed_pkgs += pkgs - - def backup_and_track_removed_pkg( - self, - pkg, - reposdir=None, - set_releasever=False, - custom_releasever=None, - varsdir=None, - ): - """Add a removed RPM pkg to the list of removed pkgs.""" - restorable_pkg = RestorablePackage(pkg) - restorable_pkg.backup( - reposdir=reposdir, - set_releasever=set_releasever, - custom_releasever=custom_releasever, - varsdir=varsdir, - ) - self.removed_pkgs.append(restorable_pkg) - - def _remove_installed_pkgs(self): - """For each package installed during conversion remove it.""" - loggerinst.task("Rollback: Remove installed packages") - remove_pkgs(self.installed_pkgs, backup=False, critical=False) - - def _install_removed_pkgs(self): - """For each package removed during conversion install it.""" - loggerinst.task("Rollback: Install removed packages") - pkgs_to_install = [] - for restorable_pkg in self.removed_pkgs: - if restorable_pkg.path is None: - loggerinst.warning("Couldn't find a backup for %s package." % restorable_pkg.name) - continue - pkgs_to_install.append(restorable_pkg.path) - - self._install_local_rpms(pkgs_to_install, replace=True, critical=False) - - def _install_local_rpms(self, pkgs_to_install, replace=False, critical=True): - """Install packages locally available.""" - - if not pkgs_to_install: - loggerinst.info("No package to install.") - return False - - cmd_param = ["rpm", "-i"] - if replace: - cmd_param.append("--replacepkgs") - - loggerinst.info("Installing packages:") - for pkg in pkgs_to_install: - loggerinst.info("\t%s" % pkg) - - cmd = cmd_param + pkgs_to_install - output, ret_code = run_subprocess(cmd, print_output=False) - if ret_code != 0: - pkgs_as_str = utils.format_sequence_as_message(pkgs_to_install) - loggerinst.debug(output.strip()) - if critical: - loggerinst.critical_no_exit("Error: Couldn't install %s packages." % pkgs_as_str) - raise exceptions.CriticalError( - id_="FAILED_TO_INSTALL_PACKAGES", - title="Couldn't install packages.", - description="While attempting to roll back changes, we encountered an unexpected failure while attempting to reinstall one or more packages that we removed as part of the conversion.", - diagnosis="Couldn't install %s packages. Command: %s Output: %s Status: %d" - % (pkgs_as_str, cmd, output, ret_code), - ) - - loggerinst.warning("Couldn't install %s packages." % pkgs_as_str) - return False - - for path in pkgs_to_install: - nvra, _ = os.path.splitext(os.path.basename(path)) - self.track_installed_pkg(nvra) - - return True - - def restore_pkgs(self): - """Restore system to the original state.""" - self._remove_installed_pkgs() - remove_orphan_folders() - self._install_removed_pkgs() +loggerinst = logging.getLogger(__name__) class BackupController: @@ -291,151 +194,4 @@ def restore(self): self.enabled = False -# Over time we want to replace this with pkghandler.RestorablePackageSet Right -# now, this is still used for removed packages. Installed packages are handled -# by pkghandler.RestorablePackageSet -class RestorablePackage: - def __init__(self, pkgname): - self.name = pkgname - self.path = None - - def backup( - self, - reposdir=None, - set_releasever=False, - custom_releasever=None, - varsdir=None, - ): - """Save version of RPM package. - - :param reposdir: Custom repositories directory to be used in the backup. - :type reposdir: str - """ - loggerinst.info("Backing up %s." % self.name) - if os.path.isdir(BACKUP_DIR): - # If we detect that the current system is an EUS release, then we - # proceed to use the hardcoded_repofiles, otherwise, we use the - # custom reposdir that comes from the method parameter. This is - # mainly because of CentOS Linux which we have hardcoded repofiles. - # If we ever put Oracle Linux repofiles to ship with convert2rhel, - # them the second part of this condition can be dropped. - if system_info.eus_system and system_info.id == "centos": - reposdir = get_hardcoded_repofiles_dir() - - # One of the reasons we hardcode repofiles pointing to archived - # repositories of older system minor versions is that we need to be - # able to download an older package version as a backup. Because for - # example the default repofiles on CentOS Linux 8.4 point only to - # 8.latest repositories that already don't contain 8.4 packages. - if not system_info.has_internet_access: - if reposdir: - loggerinst.debug( - "Not using repository files stored in %s due to the absence of internet access." % reposdir - ) - self.path = download_pkg( - self.name, - dest=BACKUP_DIR, - set_releasever=set_releasever, - custom_releasever=custom_releasever, - varsdir=varsdir, - ) - else: - if reposdir: - loggerinst.debug("Using repository files stored in %s." % reposdir) - self.path = download_pkg( - self.name, - dest=BACKUP_DIR, - set_releasever=set_releasever, - reposdir=reposdir, - custom_releasever=custom_releasever, - varsdir=varsdir, - ) - else: - loggerinst.warning("Can't access %s" % BACKUP_DIR) - - -def remove_pkgs( - pkgs_to_remove, - backup=True, - critical=True, - reposdir=None, - set_releasever=False, - custom_releasever=None, - varsdir=None, -): - """Remove packages not heeding to their dependencies.""" - # NOTE(r0x0d): This function is tied to the class - # ChangedRPMPackagesController and a couple of other places too, ideally, we - # should decide if we want to use this function as an entrypoint or the - # variable `changed_pkgs_control`, so we can move this piece of code to the - # `pkghandler.py` where it should be. Right now, if we move this code to the - # `pkghandler.py`, we have a *circular import dependency error*. @abadger - # has an implementation in mind to address some of those issues and actually - # place a controller in front of classes like this. - - if not pkgs_to_remove: - loggerinst.info("No package to remove") - return - - if backup: - # Some packages, when removed, will also remove repo files, making it - # impossible to access the repositories to download a backup. For this - # reason we first back up *all* packages and only after that we remove them. - for nevra in pkgs_to_remove: - changed_pkgs_control.backup_and_track_removed_pkg( - pkg=nevra, - reposdir=reposdir, - set_releasever=set_releasever, - custom_releasever=custom_releasever, - varsdir=varsdir, - ) - - pkgs_failed_to_remove = [] - pkgs_removed = [] - for nevra in pkgs_to_remove: - # It's necessary to remove an epoch from the NEVRA string returned by yum because the rpm command does not - # handle the epoch well and considers the package we want to remove as not installed. On the other hand, the - # epoch in NEVRA returned by dnf is handled by rpm just fine. - nvra = remove_epoch_from_yum_nevra_notation(nevra) - loggerinst.info("Removing package: %s" % nvra) - _, ret_code = run_subprocess(["rpm", "-e", "--nodeps", nvra]) - if ret_code != 0: - pkgs_failed_to_remove.append(nevra) - else: - pkgs_removed.append(nevra) - - if pkgs_failed_to_remove: - pkgs_as_str = utils.format_sequence_as_message(pkgs_failed_to_remove) - if critical: - loggerinst.critical_no_exit("Error: Couldn't remove %s." % pkgs_as_str) - raise exceptions.CriticalError( - id_="FAILED_TO_REMOVE_PACKAGES", - title="Couldn't remove packages.", - description="While attempting to roll back changes, we encountered an unexpected failure while attempting to remove one or more of the packages we installed earlier.", - diagnosis="Couldn't remove %s." % pkgs_as_str, - ) - else: - loggerinst.warning("Couldn't remove %s." % pkgs_as_str) - - return pkgs_removed - - -def remove_epoch_from_yum_nevra_notation(package_nevra): - """Remove epoch from the NEVRA string returned by yum. - - Yum prints epoch only when it's non-zero. It's printed differently by yum and dnf: - yum - epoch before name: "7:oraclelinux-release-7.9-1.0.9.el7.x86_64" - dnf - epoch before version: "oraclelinux-release-8:8.2-1.0.8.el8.x86_64" - - This function removes the epoch from the yum notation only. - It's safe to pass the dnf notation string with an epoch. This function will return it as is. - """ - epoch_match = re.search(r"^\d+:(.*)", package_nevra) - if epoch_match: - # Return NVRA without the found epoch - return epoch_match.group(1) - return package_nevra - - -changed_pkgs_control = ChangedRPMPackagesController() # pylint: disable=C0103 backup_control = BackupController() diff --git a/convert2rhel/backup/files.py b/convert2rhel/backup/files.py index 0a6ce0f177..fada72ee77 100644 --- a/convert2rhel/backup/files.py +++ b/convert2rhel/backup/files.py @@ -23,8 +23,7 @@ import shutil from convert2rhel import exceptions -from convert2rhel.backup import RestorableChange -from convert2rhel.utils import BACKUP_DIR +from convert2rhel.backup import BACKUP_DIR, RestorableChange loggerinst = logging.getLogger(__name__) diff --git a/convert2rhel/backup/packages.py b/convert2rhel/backup/packages.py index a8c584ecb2..15066f77b1 100644 --- a/convert2rhel/backup/packages.py +++ b/convert2rhel/backup/packages.py @@ -21,11 +21,12 @@ import os from convert2rhel import exceptions, utils -from convert2rhel.backup import RestorableChange, remove_pkgs +from convert2rhel.backup import BACKUP_DIR, RestorableChange +from convert2rhel.pkgmanager import call_yum_cmd # Fine to import call_yum_cmd for now, but we really should figure out a way to # split this out. -from convert2rhel.pkghandler import call_yum_cmd +from convert2rhel.repo import get_hardcoded_repofiles_dir from convert2rhel.systeminfo import system_info @@ -73,6 +74,158 @@ ) _UBI_9_REPO_PATH = os.path.join(_RHSM_TMP_DIR, "ubi_9.repo") +# Map repo_path and repo_content for each major version in UBI. +_UBI_REPO_MAPPING = { + 7: (_UBI_7_REPO_PATH, _UBI_7_REPO_CONTENT), + 8: (_UBI_8_REPO_PATH, _UBI_8_REPO_CONTENT), + 9: (_UBI_9_REPO_PATH, _UBI_9_REPO_CONTENT), +} + +# NOTE: Over time we want to replace this with pkghandler.RestorablePackageSet. +class RestorablePackage(RestorableChange): + def __init__(self, pkgs, reposdir=None, set_releasever=False, custom_releasever=None, varsdir=None): + """ + Keep control of systme packages before their removal to backup and + restore in case of rollback. + + :param pkgs list[str]: List of packages to backup. + :param reposdir str: If a custom repository directory location needs to + be used, this parameter can be set with the location for it. + :param set_releasever bool: If there is need to set the relesever while + downloading the package with yumdownloader. + :param custom_releasever str: Custom releasever in case it need to be + overwritten and it differs from the `py:system_info.releasever`. + :param varsdir str: Location to the variables directory in case the + repository files needs to interpolate variables from those folders. + """ + super(RestorablePackage, self).__init__() + + self.pkgs = pkgs + self.reposdir = reposdir + self.set_releasever = set_releasever + self.custom_releasever = custom_releasever + self.varsdir = varsdir + + self._backedup_pkgs_paths = [] + + def enable(self): + """Save version of RPMs packages. + + .. note:: + If we detect that the current system is an EUS release, then we + proceed to use the hardcoded_repofiles, otherwise, we use the + custom reposdir that comes from the method parameter. This is + mainly because of CentOS Linux which we have hardcoded repofiles. + If we ever put Oracle Linux repofiles to ship with convert2rhel, + them the second part of this condition can be dropped. + + One of the reasons we hardcode repofiles pointing to archived + repositories of older system minor versions is that we need to be + able to download an older package version as a backup. Because for + example the default repofiles on CentOS Linux 8.4 point only to + 8.latest repositories that already don't contain 8.4 packages. + """ + # Prevent multiple backup + if self.enabled: + return + + loggerinst.info("Backing up the packages: %s." % ",".join(self.pkgs)) + if os.path.isdir(BACKUP_DIR): + if system_info.eus_system and system_info.id == "centos": + self.reposdir = get_hardcoded_repofiles_dir() + + if not system_info.has_internet_access: + if self.reposdir: + loggerinst.debug( + "Not using repository files stored in %s due to the absence of internet access." % self.reposdir + ) + + for pkg in self.pkgs: + self._backedup_pkgs_paths.append( + utils.download_pkg( + pkg=pkg, + dest=BACKUP_DIR, + set_releasever=self.set_releasever, + custom_releasever=self.custom_releasever, + varsdir=self.varsdir, + ) + ) + else: + if self.reposdir: + loggerinst.debug("Using repository files stored in %s." % self.reposdir) + + for pkg in self.pkgs: + self._backedup_pkgs_paths.append( + utils.download_pkg( + pkg=pkg, + dest=BACKUP_DIR, + set_releasever=self.set_releasever, + custom_releasever=self.custom_releasever, + varsdir=self.varsdir, + reposdir=self.reposdir, + ) + ) + else: + loggerinst.warning("Can't access %s" % BACKUP_DIR) + + # Set the enabled value + super(RestorablePackage, self).enable() + + def restore(self): + """Restore system to the original state.""" + if not self.enabled: + return + + utils.remove_orphan_folders() + + loggerinst.task("Rollback: Install removed packages") + if not self._backedup_pkgs_paths: + loggerinst.warning("Couldn't find a backup for %s package." % ",".join(self.pkgs)) + return + + self._install_local_rpms(replace=True, critical=False) + + super(RestorablePackage, self).restore() + + def _install_local_rpms(self, replace=False, critical=True): + """Install packages locally available.""" + + if not self._backedup_pkgs_paths: + loggerinst.info("No package to install.") + return False + + cmd = ["rpm", "-i"] + if replace: + cmd.append("--replacepkgs") + + loggerinst.info("Installing packages:\t%s" % ", ".join(self.pkgs)) + for pkg in self._backedup_pkgs_paths: + cmd.append(pkg) + + output, ret_code = utils.run_subprocess(cmd, print_output=False) + if ret_code != 0: + pkgs_as_str = utils.format_sequence_as_message(self.pkgs) + loggerinst.debug(output.strip()) + if critical: + loggerinst.critical_no_exit("Error: Couldn't install %s packages." % pkgs_as_str) + raise exceptions.CriticalError( + id_="FAILED_TO_INSTALL_PACKAGES", + title="Couldn't install packages.", + description=( + "While attempting to roll back changes, we encountered " + "an unexpected failure while attempting to reinstall " + "one or more packages that we removed as part of the " + "conversion." + ), + diagnosis="Couldn't install %s packages. Command: %s Output: %s Status: %d" + % (pkgs_as_str, cmd, output, ret_code), + ) + + loggerinst.warning("Couldn't install %s packages." % pkgs_as_str) + return False + + return True + class RestorablePackageSet(RestorableChange): """Install a set of packages in a way that they can be uninstalled later. @@ -115,11 +268,23 @@ class RestorablePackageSet(RestorableChange): system default if we rollback the changes. """ - def __init__(self, pkgs_to_install, pkgs_to_update=None): + def __init__( + self, + pkgs_to_install, + pkgs_to_update=None, + reposdir=None, + set_releasever=False, + custom_releasever=None, + varsdir=None, + ): self.pkgs_to_install = pkgs_to_install self.pkgs_to_update = pkgs_to_update or [] self.installed_pkgs = [] self.updated_pkgs = [] + self.reposdir = reposdir + self.set_releasever = set_releasever + self.custom_releasever = custom_releasever + self.varsdir = varsdir super(RestorablePackageSet, self).__init__() @@ -149,12 +314,8 @@ def _enable(self): loggerinst.info("Downloading requested packages") all_pkgs_to_install = self.pkgs_to_install + self.pkgs_to_update - if system_info.version.major == 7: - _download_rhsm_pkgs(all_pkgs_to_install, _UBI_7_REPO_PATH, _UBI_7_REPO_CONTENT) - elif system_info.version.major == 8: - _download_rhsm_pkgs(all_pkgs_to_install, _UBI_8_REPO_PATH, _UBI_8_REPO_CONTENT) - elif system_info.version.major == 9: - _download_rhsm_pkgs(all_pkgs_to_install, _UBI_9_REPO_PATH, _UBI_9_REPO_CONTENT) + ubi_repo_path, ubi_repo_content = _UBI_REPO_MAPPING[system_info.version.major] + _download_rhsm_pkgs(all_pkgs_to_install, ubi_repo_path, ubi_repo_content) # installing the packages rpms_to_install = [os.path.join(_SUBMGR_RPMS_DIR, filename) for filename in os.listdir(_SUBMGR_RPMS_DIR)] @@ -174,8 +335,12 @@ def _enable(self): enable_repos=[], disable_repos=[], # When using the original system repos, we need YUM/DNF to expand the $releasever by itself - set_releasever=False, + set_releasever=self.set_releasever, + custom_releasever=self.custom_releasever, + reposdir=self.reposdir, + varsdir=self.varsdir, ) + if ret_code: loggerinst.critical_no_exit( "Failed to install subscription-manager packages. Check the yum output below for details:\n\n %s" @@ -214,9 +379,9 @@ def restore(self): if not self.enabled: return - loggerinst.task("Convert: Remove installed RHSM packages") + loggerinst.task("Rollback: Remove installed RHSM packages") loggerinst.info("Removing set of installed pkgs: %s" % utils.format_sequence_as_message(self.installed_pkgs)) - remove_pkgs(self.installed_pkgs, backup=False, critical=False) + utils.remove_pkgs(self.installed_pkgs, critical=False) super(RestorablePackageSet, self).restore() diff --git a/convert2rhel/hostmetering.py b/convert2rhel/hostmetering.py index 9eb8630c92..5f91c8f4b9 100644 --- a/convert2rhel/hostmetering.py +++ b/convert2rhel/hostmetering.py @@ -26,7 +26,7 @@ import os from convert2rhel import systeminfo -from convert2rhel.pkghandler import call_yum_cmd +from convert2rhel.pkgmanager import call_yum_cmd from convert2rhel.subscription import get_rhsm_facts from convert2rhel.systeminfo import system_info from convert2rhel.utils import run_subprocess diff --git a/convert2rhel/main.py b/convert2rhel/main.py index f49f1d49c6..40ef73119b 100644 --- a/convert2rhel/main.py +++ b/convert2rhel/main.py @@ -336,8 +336,6 @@ def rollback_changes(): except IndexError: backup_control_was_empty = True - backup.changed_pkgs_control.restore_pkgs() - try: backup.backup_control.pop_all() except IndexError as e: diff --git a/convert2rhel/pkghandler.py b/convert2rhel/pkghandler.py index 377626f242..854af48331 100644 --- a/convert2rhel/pkghandler.py +++ b/convert2rhel/pkghandler.py @@ -28,7 +28,6 @@ import rpm from convert2rhel import backup, pkgmanager, utils -from convert2rhel.backup import remove_pkgs from convert2rhel.backup.certs import RestorableRpmKey from convert2rhel.backup.files import RestorableFile from convert2rhel.systeminfo import system_info @@ -93,70 +92,6 @@ ) -def call_yum_cmd( - command, - args=None, - print_output=True, - enable_repos=None, - disable_repos=None, - set_releasever=True, -): - """Call yum command and optionally print its output. - The enable_repos and disable_repos function parameters accept lists and they override the default use of repos, - which is: - * --disablerepo yum option = "*" by default OR passed through a CLI option by the user - * --enablerepo yum option = is the repo enabled through subscription-manager based on a convert2rhel configuration - file for the particular system OR passed through a CLI option by the user - YUM/DNF typically expands the $releasever variable used in repofiles. However it fails to do so after we remove the - release packages (centos-release, oraclelinux-release, etc.) and before the redhat-release package is installed. - By default, for the above reason, we provide the --releasever option to each yum call. However before we remove the - release package, we need YUM/DNF to expand the variable by itself (for that, use set_releasever=False). - """ - if args is None: - args = [] - - cmd = ["yum", command, "-y"] - - # The --disablerepo yum option must be added before --enablerepo, - # otherwise the enabled repo gets disabled if --disablerepo="*" is used - repos_to_disable = [] - if isinstance(disable_repos, list): - repos_to_disable = disable_repos - else: - repos_to_disable = tool_opts.disablerepo - - for repo in repos_to_disable: - cmd.append("--disablerepo=%s" % repo) - - if set_releasever and system_info.releasever: - cmd.append("--releasever=%s" % system_info.releasever) - - # Without the release package installed, dnf can't determine the modularity platform ID. - if system_info.version.major >= 8: - cmd.append("--setopt=module_platform_id=platform:el" + str(system_info.version.major)) - - repos_to_enable = [] - if isinstance(enable_repos, list): - repos_to_enable = enable_repos - else: - # When using subscription-manager for the conversion, use those repos for the yum call that have been enabled - # through subscription-manager - repos_to_enable = system_info.get_enabled_rhel_repos() - - for repo in repos_to_enable: - cmd.append("--enablerepo=%s" % repo) - - cmd.extend(args) - - stdout, returncode = utils.run_subprocess(cmd, print_output=print_output) - # handle when yum returns non-zero code when there is nothing to do - nothing_to_do_error_exists = stdout.endswith("Error: Nothing to do\n") - if returncode == 1 and nothing_to_do_error_exists: - loggerinst.debug("Yum has nothing to do. Ignoring.") - returncode = 0 - return stdout, returncode - - def get_installed_pkgs_by_fingerprint(fingerprints, name=""): """ Return list of names of installed packages that are signed by the specific @@ -379,8 +314,9 @@ def format_pkg_info(pkgs): package_info[nevra] = {"packager": packager, "repoid": "N/A"} # Get packager length - packager_field_lengths = (len(package["packager"]) for package in package_info.values()) - max_packager_length = max(max(packager_field_lengths), len("Vendor/Packager")) + packager_field_lengths = [len(package["packager"]) for package in package_info.values()] + max_packager_field_length = max(packager_field_lengths) + max_packager_length = max(max_packager_field_length, len("Vendor/Packager")) # Get nevra length max_nvra_length = max(len(nvra) for nvra in package_info) @@ -599,35 +535,6 @@ def list_non_red_hat_pkgs_left(): loggerinst.info("All packages are now signed by Red Hat.") -def remove_pkgs_unless_from_redhat(pkgs_to_remove, backedup_reposdir, backup=True): - """Remove packages with user confirmation and backup. - - :param pkgs_to_remove: List of packages that will be removed - :type pkgs_to_remove: list[PackageInformation] - :param backup: If the package should be in a backup. Defaults to True - :type backup: bool - """ - if not pkgs_to_remove: - loggerinst.info("\nNothing to do.") - return - - loggerinst.warning("Removing the following %s packages:\n" % str(len(pkgs_to_remove))) - print_pkg_info(pkgs_to_remove) - loggerinst.info("\n") - # We're using the backed up yum repositories to prevent the following: - # - the system was registered to RHSM prior to the conversion and the system didn't have the redhat.repo generated - # for the lack of the RHSM product certificate - # - at this point convert2rhel has installed the RHSM product cert (e.g. /etc/pki/product-default/69.pem) - # - this function might be performing the first yum call convert2rhel does after cleaning yum metadata - # - the "subscription-manager" yum plugin spots that there's a new RHSM product cert and generates - # /etc/yum.repos.d/redhat.repo - # - the suddenly enabled RHEL repos cause a package backup failure - pkgs_removed = remove_pkgs(get_pkg_nevras(pkgs_to_remove), backup=backup, reposdir=backedup_reposdir) - loggerinst.debug("Successfully removed %s packages" % str(len(pkgs_to_remove))) - - return pkgs_removed - - def get_pkg_nevras(pkg_objects): """Get a list of package NEVRA strings from a list of PackageInformation objects.""" return [get_pkg_nevra(pkg_obj) for pkg_obj in pkg_objects] @@ -714,7 +621,7 @@ def install_rhel_kernel(): later on. """ loggerinst.info("Installing RHEL kernel ...") - output, ret_code = call_yum_cmd(command="install", args=["kernel"]) + output, ret_code = pkgmanager.call_yum_cmd(command="install", args=["kernel"]) if ret_code != 0: loggerinst.critical("Error occured while attempting to install the RHEL kernel") @@ -755,22 +662,22 @@ def handle_no_newer_rhel_kernel_available(): # of them - the one that has the same version as the available RHEL # kernel older = available[-1] - remove_pkgs(pkgs_to_remove=["kernel-%s" % older], backup=False) - call_yum_cmd(command="install", args=["kernel-%s" % older]) + utils.remove_pkgs(pkgs_to_remove=["kernel-%s" % older]) + pkgmanager.call_yum_cmd(command="install", args=["kernel-%s" % older]) else: replace_non_rhel_installed_kernel(installed[0]) return # Install the latest out of the available non-clashing RHEL kernels - call_yum_cmd(command="install", args=["kernel-%s" % to_install[-1]]) + pkgmanager.call_yum_cmd(command="install", args=["kernel-%s" % to_install[-1]]) def get_kernel_availability(): """Return a tuple - a list of installed kernel versions and a list of available kernel versions. """ - output, _ = call_yum_cmd(command="list", args=["--showduplicates", "kernel"], print_output=False) + output, _ = pkgmanager.call_yum_cmd(command="list", args=["--showduplicates", "kernel"], print_output=False) return (list(get_kernel(data)) for data in output.split("Available Packages")) @@ -847,9 +754,8 @@ def remove_non_rhel_kernels(): if non_rhel_kernels: loggerinst.info("Removing non-RHEL kernels\n") print_pkg_info(non_rhel_kernels) - remove_pkgs( + utils.remove_pkgs( pkgs_to_remove=[get_pkg_nvra(pkg) for pkg in non_rhel_kernels], - backup=False, ) else: loggerinst.info("None found.") @@ -937,7 +843,7 @@ def install_additional_rhel_kernel_pkgs(additional_pkgs): for name in set(pkg_names): if name != "kernel": loggerinst.info("Installing RHEL %s" % name) - call_yum_cmd("install", args=[name]) + pkgmanager.call_yum_cmd("install", args=[name]) def update_rhel_kernel(): @@ -946,7 +852,7 @@ def update_rhel_kernel(): latest available version. """ loggerinst.info("Updating RHEL kernel.") - call_yum_cmd(command="update", args=["kernel"]) + pkgmanager.call_yum_cmd(command="update", args=["kernel"]) def clear_versionlock(): @@ -966,7 +872,7 @@ def clear_versionlock(): backup.backup_control.push(RestorableFile(_VERSIONLOCK_FILE_PATH)) loggerinst.info("Clearing package versions locks...") - call_yum_cmd("versionlock", args=["clear"], print_output=False) + pkgmanager.call_yum_cmd("versionlock", args=["clear"], print_output=False) else: loggerinst.info("Usage of YUM/DNF versionlock plugin not detected.") diff --git a/convert2rhel/pkgmanager/__init__.py b/convert2rhel/pkgmanager/__init__.py index 91dee814c6..fd84774b2d 100644 --- a/convert2rhel/pkgmanager/__init__.py +++ b/convert2rhel/pkgmanager/__init__.py @@ -22,6 +22,8 @@ from contextlib import contextmanager from convert2rhel import utils +from convert2rhel.systeminfo import system_info +from convert2rhel.toolopts import tool_opts loggerinst = logging.getLogger(__name__) @@ -139,3 +141,82 @@ def rpm_db_lock(pkg_obj): pkg_obj.rpmdb.ts = None pkg_obj.rpmdb.dropCachedData() pkg_obj.rpmdb = None + + +def call_yum_cmd( + command, + args=None, + print_output=True, + enable_repos=None, + disable_repos=None, + set_releasever=True, + reposdir=None, + custom_releasever=None, + varsdir=None, +): + """Call yum command and optionally print its output. + The enable_repos and disable_repos function parameters accept lists and they override the default use of repos, + which is: + * --disablerepo yum option = "*" by default OR passed through a CLI option by the user + * --enablerepo yum option = is the repo enabled through subscription-manager based on a convert2rhel configuration + file for the particular system OR passed through a CLI option by the user + YUM/DNF typically expands the $releasever variable used in repofiles. However it fails to do so after we remove the + release packages (centos-release, oraclelinux-release, etc.) and before the redhat-release package is installed. + By default, for the above reason, we provide the --releasever option to each yum call. However before we remove the + release package, we need YUM/DNF to expand the variable by itself (for that, use set_releasever=False). + """ + if args is None: + args = [] + + cmd = ["yum", command, "-y"] + + # The --disablerepo yum option must be added before --enablerepo, + # otherwise the enabled repo gets disabled if --disablerepo="*" is used + repos_to_disable = [] + if isinstance(disable_repos, list): + repos_to_disable = disable_repos + else: + repos_to_disable = tool_opts.disablerepo + + for repo in repos_to_disable: + cmd.append("--disablerepo=%s" % repo) + + if set_releasever: + if not custom_releasever and not system_info.releasever: + raise AssertionError("custom_releasever or system_info.releasever must be set.") + + if custom_releasever: + cmd.append("--releasever=%s" % custom_releasever) + else: + cmd.append("--releasever=%s" % system_info.releasever) + + if varsdir: + cmd.append("--setopt=varsdir=%s" % varsdir) + + # Without the release package installed, dnf can't determine the modularity platform ID. + if system_info.version.major >= 8: + cmd.append("--setopt=module_platform_id=platform:el" + str(system_info.version.major)) + + repos_to_enable = [] + if isinstance(enable_repos, list): + repos_to_enable = enable_repos + else: + # When using subscription-manager for the conversion, use those repos for the yum call that have been enabled + # through subscription-manager + repos_to_enable = system_info.get_enabled_rhel_repos() + + for repo in repos_to_enable: + cmd.append("--enablerepo=%s" % repo) + + if reposdir: + cmd.append("--setopt=reposdir=%s" % reposdir) + + cmd.extend(args) + + stdout, returncode = utils.run_subprocess(cmd, print_output=print_output) + # handle when yum returns non-zero code when there is nothing to do + nothing_to_do_error_exists = stdout.endswith("Error: Nothing to do\n") + if returncode == 1 and nothing_to_do_error_exists: + loggerinst.debug("Yum has nothing to do. Ignoring.") + returncode = 0 + return stdout, returncode diff --git a/convert2rhel/pkgmanager/handlers/yum/__init__.py b/convert2rhel/pkgmanager/handlers/yum/__init__.py index 44226b315b..ecdbaa3c84 100644 --- a/convert2rhel/pkgmanager/handlers/yum/__init__.py +++ b/convert2rhel/pkgmanager/handlers/yum/__init__.py @@ -21,16 +21,15 @@ import logging import os import re -import shutil -from convert2rhel import exceptions, pkgmanager, utils -from convert2rhel.backup import remove_pkgs +from convert2rhel import backup, exceptions, pkgmanager, utils +from convert2rhel.backup.packages import RestorablePackage from convert2rhel.pkghandler import get_system_packages_for_replacement from convert2rhel.pkgmanager.handlers.base import TransactionHandlerBase from convert2rhel.pkgmanager.handlers.yum.callback import PackageDownloadCallback, TransactionDisplayCallback from convert2rhel.repo import DEFAULT_YUM_REPOFILE_DIR, DEFAULT_YUM_VARS_DIR from convert2rhel.systeminfo import system_info -from convert2rhel.utils import BACKUP_DIR, run_as_child_process +from convert2rhel.utils import remove_pkgs loggerinst = logging.getLogger(__name__) @@ -60,6 +59,7 @@ def _resolve_yum_problematic_dependencies(output): loggerinst.debug("Dependency resolution failed:\n- %s" % "\n- ".join(output)) else: loggerinst.debug("Dependency resolution failed with no detailed message reported by yum.") + for package in output: resolve_error = re.findall(EXTRACT_PKG_FROM_YUM_DEPSOLVE, str(package)) if resolve_error: @@ -72,18 +72,19 @@ def _resolve_yum_problematic_dependencies(output): "Removing problematic packages to continue with the conversion:\n%s", "\n".join(packages_to_remove), ) - backedup_reposdir = os.path.join(BACKUP_DIR, hashlib.md5(DEFAULT_YUM_REPOFILE_DIR.encode()).hexdigest()) - backedup_yum_varsdir = os.path.join(BACKUP_DIR, hashlib.md5(DEFAULT_YUM_VARS_DIR.encode()).hexdigest()) - - remove_pkgs( - pkgs_to_remove=packages_to_remove, - backup=True, - critical=True, - set_releasever=True, - reposdir=backedup_reposdir, - custom_releasever=system_info.version.major, - varsdir=backedup_yum_varsdir, + backedup_reposdir = os.path.join(backup.BACKUP_DIR, hashlib.md5(DEFAULT_YUM_REPOFILE_DIR.encode()).hexdigest()) + backedup_yum_varsdir = os.path.join(backup.BACKUP_DIR, hashlib.md5(DEFAULT_YUM_VARS_DIR.encode()).hexdigest()) + + backup.backup_control.push( + RestorablePackage( + pkgs=packages_to_remove, + reposdir=backedup_reposdir, + set_releasever=True, + custom_releasever=system_info.version.major, + varsdir=backedup_yum_varsdir, + ) ) + remove_pkgs(pkgs_to_remove=packages_to_remove, critical=True) loggerinst.debug("Finished backing up and removing the packages.") else: @@ -197,7 +198,6 @@ def _perform_operations(self): `ReinstallInstallError`. """ original_os_pkgs = get_system_packages_for_replacement() - self._set_up_base() self._enable_repos() loggerinst.info("Adding %s packages to the yum transaction set.", system_info.name) @@ -237,7 +237,7 @@ def _perform_operations(self): diagnosis="Repository mirrors failed with error %s." % (str(e)), ) - def _resolve_dependencies(self, validate_transaction): + def _resolve_dependencies(self): """Try to resolve the transaction dependencies. This method will try to resolve the dependencies of the packages that @@ -262,35 +262,17 @@ def _resolve_dependencies(self, validate_transaction): errors are "thrown" for the user, you will need to loop until the point you don't have any more errors. - :param validate_transaction: Determines if the transaction needs to be - validated or not. - :type validate_transaction: bool - - :return: A boolean indicating if it was successful or not. - :rtype: bool + :return: If the base.resolveDeps() method returns a message, we will + return that message, otherwise, return None. + :rtype: str | None """ loggerinst.info("Resolving the dependencies of the packages in the yum transaction set.") ret_code, msg = self._base.resolveDeps() if ret_code == 1: - # For the return code 1, yum can output two kinds of error, one being - # that it reached the limit for depsolving, and the actual dependencies - # that caused an problem. - # If we reach the limit for depsolving, just return False. - if "Depsolving loop limit reached" in msg: - return False - # If the message is the not the depsolving limit, then we need to - # resolve the problematic dependencies. - else: - # We want to fail earlier in the process, so let's check for this - # only when testing the transaction. - if validate_transaction: - _resolve_yum_problematic_dependencies(msg) - - # Return False anyway because the depsolving failed. - return False - - return True + return msg + + return None def _process_transaction(self, validate_transaction): """Internal method to process the transaction. @@ -345,7 +327,6 @@ def _process_transaction(self, validate_transaction): else: loggerinst.info("System packages replaced successfully.") - @utils.run_as_child_process def run_transaction(self, validate_transaction=False): """Run the yum transaction. @@ -353,15 +334,51 @@ def run_transaction(self, validate_transaction=False): true, it means the transaction will not be executed, but rather verify everything and do an early return. + :param validate_transaction: Determines if the transaction needs to be + validated or not. + :type validate_transaction: bool + :raises CriticalError: If we can't resolve the transaction dependencies. + """ + resolve_deps_finished = False + # Do not allow this to loop until eternity. + attempts = 0 + try: + while attempts <= MAX_NUM_OF_ATTEMPTS_TO_RESOLVE_DEPS: + self._set_up_base() + messages = self._run_transaction_subprocess(validate_transaction) + if messages: + if "Depsolving loop limit reached" not in messages and validate_transaction: + _resolve_yum_problematic_dependencies(messages) + + loggerinst.info("Retrying to resolve dependencies %s", attempts) + attempts += 1 + else: + resolve_deps_finished = True + break + + if not resolve_deps_finished: + loggerinst.critical_no_exit("Failed to resolve dependencies in the transaction.") + raise exceptions.CriticalError( + id_="FAILED_TO_RESOLVE_DEPENDENCIES", + title="Failed to resolve dependencies.", + description="During package transaction yum failed to resolve the necessary dependencies needed for a package replacement.", + ) + finally: + self._close_yum_base() + + @utils.run_as_child_process + def _run_transaction_subprocess(self, validate_transaction): + """Run the necessary transaction operations under a subprocess. + .. important:: This function is being executed in a child process so we will be able to raise SIGINT or any other signal that is sent to the main process. - The function calls here do not affect the others subprocess - calls that are called after this function during the conversion, - but, it does affect the signal handling while the user tries to - send that signal while this function is executing. + The function calls here do not affect the others subprocess calls + that are called after this function during the conversion, but, it + does affect the signal handling while the user tries to send that + signal while this function is executing. ..notes:: The implementation of this yum transaction is different from the @@ -370,14 +387,14 @@ def run_transaction(self, validate_transaction=False): this, we need to loop through a couple of times until we know that all of the dependencies are resolved without problems. - You might wonder "why not remove the packages that caused a - failure and loop through the dep solving again?" Well. Since - we are removing the problematic packages using `rpm` and not some - specific method in the transaction itself, yum doesn't know that - something has changed (The resolveDeps() function doesn't refresh - if something else happens outside the transaction), in order to - make sure that we won't have any problems with our transaction, it - is easier to loop through everything again and just recreate the + You might wonder "why not remove the packages that caused a failure + and loop through the dep solving again?" Well. Since we are + removing the problematic packages using `rpm` and not some specific + method in the transaction itself, yum doesn't know that something + has changed (The resolveDeps() function doesn't refresh if + something else happens outside the transaction), in order to make + sure that we won't have any problems with our transaction, it is + easier to loop through everything again and just recreate the transaction, so yum will keep track of what's changed. This function should loop max 3 times to get to the point where our @@ -391,30 +408,14 @@ def run_transaction(self, validate_transaction=False): :param vaidate_transaction: Determines if the transaction needs to be validated or not. :type validate_transaction: bool - :raises SystemExit: If we can't resolve the transaction dependencies. + :returns str | None: If any messages are raised from the dependency + resolve methods, we return that to the caller. Otherwise, we return + None. """ - resolve_deps_finished = False - # Do not allow this to loop until eternity. - attempts = 0 - try: - while attempts <= MAX_NUM_OF_ATTEMPTS_TO_RESOLVE_DEPS: - self._perform_operations() - resolved = self._resolve_dependencies(validate_transaction) - if not resolved: - loggerinst.info("Retrying to resolve dependencies %s", attempts) - attempts += 1 - else: - resolve_deps_finished = True - break - - if not resolve_deps_finished: - loggerinst.critical_no_exit("Failed to resolve dependencies in the transaction.") - raise exceptions.CriticalError( - id_="FAILED_TO_RESOLVE_DEPENDENCIES", - title="Failed to resolve dependencies.", - description="During package transaction yum failed to resolve the necessary dependencies needed for a package replacement.", - ) + self._perform_operations() + messages = self._resolve_dependencies() + if not messages: self._process_transaction(validate_transaction) - finally: - self._close_yum_base() + + return messages diff --git a/convert2rhel/subscription.py b/convert2rhel/subscription.py index b5225ebde6..b813fc62dc 100644 --- a/convert2rhel/subscription.py +++ b/convert2rhel/subscription.py @@ -17,6 +17,7 @@ __metaclass__ = type +import hashlib import json import logging import os @@ -32,6 +33,7 @@ from convert2rhel import backup, exceptions, i18n, pkghandler, utils from convert2rhel.backup.packages import RestorablePackageSet from convert2rhel.redhatrelease import os_release_file +from convert2rhel.repo import DEFAULT_DNF_VARS_DIR, DEFAULT_YUM_REPOFILE_DIR, DEFAULT_YUM_VARS_DIR from convert2rhel.systeminfo import system_info from convert2rhel.toolopts import _should_subscribe, tool_opts @@ -561,7 +563,18 @@ def install_rhel_subscription_manager(pkgs_to_install, pkgs_to_upgrade=None): """ pkgs_to_upgrade = pkgs_to_upgrade or [] - installed_pkg_set = RestorablePackageSet(pkgs_to_install, pkgs_to_upgrade) + backedup_reposdir = os.path.join(backup.BACKUP_DIR, hashlib.md5(DEFAULT_YUM_REPOFILE_DIR.encode()).hexdigest()) + varsdir = DEFAULT_YUM_VARS_DIR if system_info.version.major == 7 else DEFAULT_DNF_VARS_DIR + backedup_varsdir = os.path.join(backup.BACKUP_DIR, hashlib.md5(varsdir.encode()).hexdigest()) + + installed_pkg_set = RestorablePackageSet( + pkgs_to_install, + pkgs_to_upgrade, + reposdir=backedup_reposdir, + custom_releasever=system_info.version.major, + set_releasever=True, + varsdir=backedup_varsdir, + ) backup.backup_control.push(installed_pkg_set) diff --git a/convert2rhel/unit_tests/__init__.py b/convert2rhel/unit_tests/__init__.py index 35160e1dbe..832190881c 100644 --- a/convert2rhel/unit_tests/__init__.py +++ b/convert2rhel/unit_tests/__init__.py @@ -22,6 +22,8 @@ import os import sys +from typing import Any + import pytest import six @@ -33,6 +35,7 @@ initialize, main, pkghandler, + pkgmanager, subscription, systeminfo, toolopts, @@ -311,25 +314,31 @@ class SummaryAsJsonMocked(MockFunctionObject): # -class RemovePkgsMocked(MockFunctionObject): +class RestorablePackageMocked(MockFunctionObject): """ - Mock for the remove_pkgs function. + Mock for the RestorablePackage class. This differs from Mock in that it: * Makes it easy to check just the pkgs passed in to remove. """ - spec = backup.remove_pkgs + spec = backup.packages.RestorablePackage def __init__(self, **kwargs): self.pkgs = None - super(RemovePkgsMocked, self).__init__(**kwargs) + super(RestorablePackageMocked, self).__init__(**kwargs) - def __call__(self, pkgs_to_remove, *args, **kwargs): - self.pkgs = pkgs_to_remove + def __call__(self, pkgs, reposdir, set_releasever, custom_releasever, varsdir, *args, **kwargs): + self.pkgs = pkgs + self.reposdir = reposdir + self.set_releasever = set_releasever + self.custom_releasever = custom_releasever + self.varsdir = varsdir - return super(RemovePkgsMocked, self).__call__(pkgs_to_remove, *args, **kwargs) + return super(RestorablePackageMocked, self).__call__( + pkgs, reposdir, set_releasever, custom_releasever, vars, *args, **kwargs + ) # @@ -388,7 +397,7 @@ class CallYumCmdMocked(MockFunctionObject): * Has special handling to make failing a single time and then succeeding easy. """ - spec = pkghandler.call_yum_cmd + spec = pkgmanager.call_yum_cmd def __init__(self, return_code=0, return_string="Test output", fail_once=False, **kwargs): self.command = "" @@ -505,10 +514,6 @@ class FormatPkgInfoMocked(MockFunctionObject): spec = pkghandler.format_pkg_info -class RemovePkgsUnlessFromRedhatMocked(MockFunctionObject): - spec = pkghandler.remove_pkgs_unless_from_redhat - - # # subscription mocks # @@ -561,6 +566,27 @@ class CLIMocked(MockFunctionObject): # +class RemovePkgsMocked(MockFunctionObject): + """ + Mock for the remove_pkgs function. + + This differs from Mock in that it: + * Makes it easy to check just the pkgs passed in to remove. + """ + + spec = utils.remove_pkgs + + def __init__(self, **kwargs): + self.pkgs = None + + super(RemovePkgsMocked, self).__init__(**kwargs) + + def __call__(self, pkgs_to_remove, *args, **kwargs): + self.pkgs = pkgs_to_remove + + return super(RemovePkgsMocked, self).__call__(pkgs_to_remove, *args, **kwargs) + + class DownloadPkgMocked(MockFunctionObject): """ Mock for the download_pkgs function. @@ -817,3 +843,14 @@ def __init__(self, exception=None): def restore(self): super(ErrorOnRestoreRestorable, self).restore() raise self.exception + + +class RestorablePackageMock(MinimalRestorable): + def __init__(self, pkg_name=None, reposdir=None, set_releasever=False, custom_releasever=None, varsdir=None): + super(RestorablePackageMock, self).__init__() + + self.pkg_name = pkg_name + self.reposdir = reposdir + self.set_releasever = set_releasever + self.custom_releasever = custom_releasever + self.varsdir = varsdir diff --git a/convert2rhel/unit_tests/actions/actions_test.py b/convert2rhel/unit_tests/actions/actions_test.py index 49c8dd2f25..461dc774a7 100644 --- a/convert2rhel/unit_tests/actions/actions_test.py +++ b/convert2rhel/unit_tests/actions/actions_test.py @@ -58,27 +58,34 @@ class TestAction: ( # Set one result field ( - dict(level="SUCCESS", id="SUCCESS"), - dict(level="SUCCESS", id="SUCCESS", title="", description="", diagnosis="", remediations=""), + {"level": "SUCCESS", "id": "SUCCESS"}, + { + "level": "SUCCESS", + "id": "SUCCESS", + "title": "", + "description": "", + "diagnosis": "", + "remediations": "", + }, ), # Set all result fields ( - dict( - level="ERROR", - id="ERRORCASE", - title="Problem detected", - description="problem", - diagnosis="detected", - remediations="move on", - ), - dict( - level="ERROR", - id="ERRORCASE", - title="Problem detected", - description="problem", - diagnosis="detected", - remediations="move on", - ), + { + "level": "ERROR", + "id": "ERRORCASE", + "title": "Problem detected", + "description": "problem", + "diagnosis": "detected", + "remediations": "move on", + }, + { + "level": "ERROR", + "id": "ERRORCASE", + "title": "Problem detected", + "description": "problem", + "diagnosis": "detected", + "remediations": "move on", + }, ), ), ) @@ -738,18 +745,18 @@ class TestRunActions: [], ), { - "One": dict( - messages=[], - result=dict( - level=STATUS_CODE["SUCCESS"], - id="SUCCESS", - title="", - description="", - diagnosis="", - remediations="", - variables={}, - ), - ) + "One": { + "messages": [], + "result": { + "level": STATUS_CODE["SUCCESS"], + "id": "SUCCESS", + "title": "", + "description": "", + "diagnosis": "", + "remediations": "", + "variables": {}, + }, + } }, ), ( @@ -775,30 +782,30 @@ class TestRunActions: [], ), { - "One": dict( - messages=[], - result=dict( - level=STATUS_CODE["SUCCESS"], - id="SUCCESS", - title="", - description="", - diagnosis="", - remediations="", - variables={}, - ), - ), - "Two": dict( - messages=[], - result=dict( - level=STATUS_CODE["SUCCESS"], - id="SUCCESS", - title="", - description="", - diagnosis="", - remediations="", - variables={}, - ), - ), + "One": { + "messages": [], + "result": { + "level": STATUS_CODE["SUCCESS"], + "id": "SUCCESS", + "title": "", + "description": "", + "diagnosis": "", + "remediations": "", + "variables": {}, + }, + }, + "Two": { + "messages": [], + "result": { + "level": STATUS_CODE["SUCCESS"], + "id": "SUCCESS", + "title": "", + "description": "", + "diagnosis": "", + "remediations": "", + "variables": {}, + }, + }, }, ), # Single Failures @@ -822,18 +829,18 @@ class TestRunActions: [], ), { - "One": dict( - messages=[], - result=dict( - level=STATUS_CODE["ERROR"], - id="SOME_ERROR", - title="Error", - description="Action error", - diagnosis="User error", - remediations="move on", - variables={}, - ), - ), + "One": { + "messages": [], + "result": { + "level": STATUS_CODE["ERROR"], + "id": "SOME_ERROR", + "title": "Error", + "description": "Action error", + "diagnosis": "User error", + "remediations": "move on", + "variables": {}, + }, + }, }, ), ( @@ -856,18 +863,18 @@ class TestRunActions: [], ), { - "One": dict( - messages=[], - result=dict( - level=STATUS_CODE["OVERRIDABLE"], - id="SOME_ERROR", - title="Overridable", - description="Action overridable", - diagnosis="User overridable", - remediations="move on", - variables={}, - ), - ), + "One": { + "messages": [], + "result": { + "level": STATUS_CODE["OVERRIDABLE"], + "id": "SOME_ERROR", + "title": "Overridable", + "description": "Action overridable", + "diagnosis": "User overridable", + "remediations": "move on", + "variables": {}, + }, + }, }, ), ( @@ -890,18 +897,18 @@ class TestRunActions: ], ), { - "One": dict( - messages=[], - result=dict( - level=STATUS_CODE["SKIP"], - id="SOME_ERROR", - title="Skip", - description="Action skip", - diagnosis="User skip", - remediations="move on", - variables={}, - ), - ), + "One": { + "messages": [], + "result": { + "level": STATUS_CODE["SKIP"], + "id": "SOME_ERROR", + "title": "Skip", + "description": "Action skip", + "diagnosis": "User skip", + "remediations": "move on", + "variables": {}, + }, + }, }, ), # Mixture of failures and successes. @@ -940,42 +947,42 @@ class TestRunActions: ], ), { - "One": dict( - messages=[], - result=dict( - level=STATUS_CODE["ERROR"], - id="ERROR_ID", - title="Error", - description="Action error", - diagnosis="User error", - remediations="move on", - variables={}, - ), - ), - "Two": dict( - messages=[], - result=dict( - level=STATUS_CODE["SKIP"], - id="SKIP_ID", - title="Skip", - description="Action skip", - diagnosis="User skip", - remediations="move on", - variables={}, - ), - ), - "Three": dict( - messages=[], - result=dict( - level=STATUS_CODE["SUCCESS"], - id="SUCCESS", - title="", - description="", - diagnosis="", - remediations="", - variables={}, - ), - ), + "One": { + "messages": [], + "result": { + "level": STATUS_CODE["ERROR"], + "id": "ERROR_ID", + "title": "Error", + "description": "Action error", + "diagnosis": "User error", + "remediations": "move on", + "variables": {}, + }, + }, + "Two": { + "messages": [], + "result": { + "level": STATUS_CODE["SKIP"], + "id": "SKIP_ID", + "title": "Skip", + "description": "Action skip", + "diagnosis": "User skip", + "remediations": "move on", + "variables": {}, + }, + }, + "Three": { + "messages": [], + "result": { + "level": STATUS_CODE["SUCCESS"], + "id": "SUCCESS", + "title": "", + "description": "", + "diagnosis": "", + "remediations": "", + "variables": {}, + }, + }, }, ), ), @@ -1015,28 +1022,28 @@ def test_run_actions(self, action_results, expected, monkeypatch): [], ), { - "One": dict( - messages=[ - dict( - level=STATUS_CODE["WARNING"], - id="WARNING_ID", - title="Warning", - description="Action warning", - diagnosis="User warning", - remediations="move on", - variables={}, - ) + "One": { + "messages": [ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediations": "move on", + "variables": {}, + } ], - result=dict( - level=STATUS_CODE["SUCCESS"], - id="SUCCESS", - title="", - description="", - diagnosis="", - remediations="", - variables={}, - ), - ) + "result": { + "level": STATUS_CODE["SUCCESS"], + "id": "SUCCESS", + "title": "", + "description": "", + "diagnosis": "", + "remediations": "", + "variables": {}, + }, + } }, ), ( @@ -1080,50 +1087,50 @@ def test_run_actions(self, action_results, expected, monkeypatch): [], ), { - "One": dict( - messages=[ - dict( - level=STATUS_CODE["WARNING"], - id="WARNING_ID", - title="Warning", - description="Action warning", - diagnosis="User warning", - remediations="move on", - variables={}, - ) + "One": { + "messages": [ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediations": "move on", + "variables": {}, + } ], - result=dict( - level=STATUS_CODE["SUCCESS"], - id="SUCCESS", - title="", - description="", - diagnosis="", - remediations="", - variables={}, - ), - ), - "Two": dict( - messages=[ - dict( - level=STATUS_CODE["WARNING"], - id="WARNING_ID", - title="Warning", - description="Action warning", - diagnosis="User warning", - remediations="move on", - variables={}, - ) + "result": { + "level": STATUS_CODE["SUCCESS"], + "id": "SUCCESS", + "title": "", + "description": "", + "diagnosis": "", + "remediations": "", + "variables": {}, + }, + }, + "Two": { + "messages": [ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediations": "move on", + "variables": {}, + } ], - result=dict( - level=STATUS_CODE["SUCCESS"], - id="SUCCESS", - title="", - description="", - diagnosis="", - remediations="", - variables={}, - ), - ), + "result": { + "level": STATUS_CODE["SUCCESS"], + "id": "SUCCESS", + "title": "", + "description": "", + "diagnosis": "", + "remediations": "", + "variables": {}, + }, + }, }, ), # Single Failures @@ -1156,28 +1163,28 @@ def test_run_actions(self, action_results, expected, monkeypatch): [], ), { - "One": dict( - messages=[ - dict( - level=STATUS_CODE["WARNING"], - id="WARNING_ID", - title="Warning", - description="Action warning", - diagnosis="User warning", - remediations="move on", - variables={}, - ) + "One": { + "messages": [ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediations": "move on", + "variables": {}, + } ], - result=dict( - level=STATUS_CODE["ERROR"], - id="SOME_ERROR", - title="Error", - description="Action error", - diagnosis="User error", - remediations="move on", - variables={}, - ), - ), + "result": { + "level": STATUS_CODE["ERROR"], + "id": "SOME_ERROR", + "title": "Error", + "description": "Action error", + "diagnosis": "User error", + "remediations": "move on", + "variables": {}, + }, + }, }, ), ( @@ -1209,28 +1216,28 @@ def test_run_actions(self, action_results, expected, monkeypatch): [], ), { - "One": dict( - messages=[ - dict( - level=STATUS_CODE["WARNING"], - id="WARNING_ID", - title="Warning", - description="Action warning", - diagnosis="User warning", - remediations="move on", - variables={}, - ) + "One": { + "messages": [ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediations": "move on", + "variables": {}, + } ], - result=dict( - level=STATUS_CODE["OVERRIDABLE"], - id="SOME_ERROR", - title="Overridable", - description="Action overridable", - diagnosis="User overridable", - remediations="move on", - variables={}, - ), - ), + "result": { + "level": STATUS_CODE["OVERRIDABLE"], + "id": "SOME_ERROR", + "title": "Overridable", + "description": "Action overridable", + "diagnosis": "User overridable", + "remediations": "move on", + "variables": {}, + }, + }, }, ), ( @@ -1262,28 +1269,28 @@ def test_run_actions(self, action_results, expected, monkeypatch): ], ), { - "One": dict( - messages=[ - dict( - level=STATUS_CODE["WARNING"], - id="WARNING_ID", - title="Warning", - description="Action warning", - diagnosis="User warning", - remediations="move on", - variables={}, - ) + "One": { + "messages": [ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediations": "move on", + "variables": {}, + } ], - result=dict( - level=STATUS_CODE["SKIP"], - id="SOME_ERROR", - title="Skip", - description="Action skip", - diagnosis="User skip", - remediations="move on", - variables={}, - ), - ), + "result": { + "level": STATUS_CODE["SKIP"], + "id": "SOME_ERROR", + "title": "Skip", + "description": "Action skip", + "diagnosis": "User skip", + "remediations": "move on", + "variables": {}, + }, + }, }, ), # Mixture of failures and successes. @@ -1353,72 +1360,72 @@ def test_run_actions(self, action_results, expected, monkeypatch): ], ), { - "One": dict( - messages=[ - dict( - level=STATUS_CODE["WARNING"], - id="WARNING_ID", - title="Warning", - description="Action warning", - diagnosis="User warning", - remediations="move on", - variables={}, - ) + "One": { + "messages": [ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediations": "move on", + "variables": {}, + } ], - result=dict( - level=STATUS_CODE["ERROR"], - id="ERROR_ID", - title="Error", - description="Action error", - diagnosis="User error", - remediations="move on", - variables={}, - ), - ), - "Two": dict( - messages=[ - dict( - level=STATUS_CODE["WARNING"], - id="WARNING_ID", - title="Warning", - description="Action warning", - diagnosis="User warning", - remediations="move on", - variables={}, - ) + "result": { + "level": STATUS_CODE["ERROR"], + "id": "ERROR_ID", + "title": "Error", + "description": "Action error", + "diagnosis": "User error", + "remediations": "move on", + "variables": {}, + }, + }, + "Two": { + "messages": [ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediations": "move on", + "variables": {}, + } ], - result=dict( - level=STATUS_CODE["SKIP"], - id="SKIP_ID", - title="Skip", - description="Action skip", - diagnosis="User skip", - remediations="move on", - variables={}, - ), - ), - "Three": dict( - messages=[ - dict( - level=STATUS_CODE["WARNING"], - id="WARNING_ID", - title="Warning", - description="Action warning", - diagnosis="User warning", - remediations="move on", - variables={}, - ) + "result": { + "level": STATUS_CODE["SKIP"], + "id": "SKIP_ID", + "title": "Skip", + "description": "Action skip", + "diagnosis": "User skip", + "remediations": "move on", + "variables": {}, + }, + }, + "Three": { + "messages": [ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediations": "move on", + "variables": {}, + } ], - result=dict( - level=STATUS_CODE["SUCCESS"], - id="SUCCESS", - title="", - description="", - diagnosis="", - remediations="", - variables={}, - ), - ), + "result": { + "level": STATUS_CODE["SUCCESS"], + "id": "SUCCESS", + "title": "", + "description": "", + "diagnosis": "", + "remediations": "", + "variables": {}, + }, + }, }, ), ), @@ -1448,10 +1455,10 @@ def test_dependency_errors(self, monkeypatch, caplog): class TestFindFailedActions: test_results = { - "BAD": dict(result=dict(level=STATUS_CODE["ERROR"], id="ERROR", message="Explosion")), - "BAD2": dict(result=dict(level=STATUS_CODE["OVERRIDABLE"], id="OVERRIDABLE", message="Explosion")), - "BAD3": dict(result=dict(level=STATUS_CODE["SKIP"], id="SKIP", message="Explosion")), - "GOOD": dict(result=dict(level=STATUS_CODE["SUCCESS"], id="SUCCESS", message="No Error here")), + "BAD": {"result": {"level": STATUS_CODE["ERROR"], "id": "ERROR", "message": "Explosion"}}, + "BAD2": {"result": {"level": STATUS_CODE["OVERRIDABLE"], "id": "OVERRIDABLE", "message": "Explosion"}}, + "BAD3": {"result": {"level": STATUS_CODE["SKIP"], "id": "SKIP", "message": "Explosion"}}, + "GOOD": {"result": {"level": STATUS_CODE["SUCCESS"], "id": "SUCCESS", "message": "No Error here"}}, } @pytest.mark.parametrize( @@ -1479,15 +1486,15 @@ class TestActionClasses: None, None, None, - dict( - id="SUCCESS", - level=STATUS_CODE["SUCCESS"], - title="", - description="", - diagnosis="", - remediations="", - variables={}, - ), + { + "id": "SUCCESS", + "level": STATUS_CODE["SUCCESS"], + "title": "", + "description": "", + "diagnosis": "", + "remediations": "", + "variables": {}, + }, ), ( "SKIP_ID", @@ -1496,15 +1503,15 @@ class TestActionClasses: "skip description", "skip diagnosis", "skip remediations", - dict( - id="SKIP_ID", - level=STATUS_CODE["SKIP"], - title="Skip message", - description="skip description", - diagnosis="skip diagnosis", - remediations="skip remediations", - variables={}, - ), + { + "id": "SKIP_ID", + "level": STATUS_CODE["SKIP"], + "title": "Skip message", + "description": "skip description", + "diagnosis": "skip diagnosis", + "remediations": "skip remediations", + "variables": {}, + }, ), ( "OVERRIDABLE_ID", @@ -1513,15 +1520,15 @@ class TestActionClasses: "overridable description", "overridable diagnosis", "overridable remediations", - dict( - id="OVERRIDABLE_ID", - level=STATUS_CODE["OVERRIDABLE"], - title="Overridable message", - description="overridable description", - diagnosis="overridable diagnosis", - remediations="overridable remediations", - variables={}, - ), + { + "id": "OVERRIDABLE_ID", + "level": STATUS_CODE["OVERRIDABLE"], + "title": "Overridable message", + "description": "overridable description", + "diagnosis": "overridable diagnosis", + "remediations": "overridable remediations", + "variables": {}, + }, ), ( "ERROR_ID", @@ -1530,15 +1537,15 @@ class TestActionClasses: "error description", "error diagnosis", "error remediations", - dict( - id="ERROR_ID", - level=STATUS_CODE["ERROR"], - title="Error message", - description="error description", - diagnosis="error diagnosis", - remediations="error remediations", - variables={}, - ), + { + "id": "ERROR_ID", + "level": STATUS_CODE["ERROR"], + "title": "Error message", + "description": "error description", + "diagnosis": "error diagnosis", + "remediations": "error remediations", + "variables": {}, + }, ), ), ) @@ -1626,15 +1633,15 @@ def test_action_message_exceptions(self, level, id, title, description, diagnosi "warning description", "warning diagnosis", "warning remediations", - dict( - id="WARNING_ID", - level=STATUS_CODE["WARNING"], - title="Warning message", - description="warning description", - diagnosis="warning diagnosis", - remediations="warning remediations", - variables={}, - ), + { + "id": "WARNING_ID", + "level": STATUS_CODE["WARNING"], + "title": "Warning message", + "description": "warning description", + "diagnosis": "warning diagnosis", + "remediations": "warning remediations", + "variables": {}, + }, ), ( "INFO_ID", @@ -1643,15 +1650,15 @@ def test_action_message_exceptions(self, level, id, title, description, diagnosi "info description", "info diagnosis", "info remediations", - dict( - id="INFO_ID", - level=STATUS_CODE["INFO"], - title="Info message", - description="info description", - diagnosis="info diagnosis", - remediations="info remediations", - variables={}, - ), + { + "id": "INFO_ID", + "level": STATUS_CODE["INFO"], + "title": "Info message", + "description": "info description", + "diagnosis": "info diagnosis", + "remediations": "info remediations", + "variables": {}, + }, ), ( "INFO_ID", @@ -1660,15 +1667,15 @@ def test_action_message_exceptions(self, level, id, title, description, diagnosi "info description", "info diagnosis", "info remediations", - dict( - id="INFO_ID", - level=STATUS_CODE["INFO"], - title="Info message", - description="info description", - diagnosis="info diagnosis", - remediations="info remediations", - variables={}, - ), + { + "id": "INFO_ID", + "level": STATUS_CODE["INFO"], + "title": "Info message", + "description": "info description", + "diagnosis": "info diagnosis", + "remediations": "info remediations", + "variables": {}, + }, ), ), ) @@ -1778,15 +1785,15 @@ def test_action_result_exceptions( None, None, {}, - dict( - id="SUCCESS_ID", - level=STATUS_CODE["SUCCESS"], - title="", - description="", - diagnosis="", - remediations="", - variables={}, - ), + { + "id": "SUCCESS_ID", + "level": STATUS_CODE["SUCCESS"], + "title": "", + "description": "", + "diagnosis": "", + "remediations": "", + "variables": {}, + }, ), ( "SKIP_ID", @@ -1796,15 +1803,15 @@ def test_action_result_exceptions( "skip diagnosis", "skip remediations", {}, - dict( - id="SKIP_ID", - level=STATUS_CODE["SKIP"], - title="Skip", - description="skip description", - diagnosis="skip diagnosis", - remediations="skip remediations", - variables={}, - ), + { + "id": "SKIP_ID", + "level": STATUS_CODE["SKIP"], + "title": "Skip", + "description": "skip description", + "diagnosis": "skip diagnosis", + "remediations": "skip remediations", + "variables": {}, + }, ), ( "OVERRIDABLE_ID", @@ -1814,15 +1821,15 @@ def test_action_result_exceptions( "overridable diagnosis", "overridable remediations", {}, - dict( - id="OVERRIDABLE_ID", - level=STATUS_CODE["OVERRIDABLE"], - title="Overridable", - description="overridable description", - diagnosis="overridable diagnosis", - remediations="overridable remediations", - variables={}, - ), + { + "id": "OVERRIDABLE_ID", + "level": STATUS_CODE["OVERRIDABLE"], + "title": "Overridable", + "description": "overridable description", + "diagnosis": "overridable diagnosis", + "remediations": "overridable remediations", + "variables": {}, + }, ), ( "ERROR_ID", @@ -1832,15 +1839,15 @@ def test_action_result_exceptions( "error diagnosis", "error remediations", {}, - dict( - id="ERROR_ID", - level=STATUS_CODE["ERROR"], - title="Error", - description="error description", - diagnosis="error diagnosis", - remediations="error remediations", - variables={}, - ), + { + "id": "ERROR_ID", + "level": STATUS_CODE["ERROR"], + "title": "Error", + "description": "error description", + "diagnosis": "error diagnosis", + "remediations": "error remediations", + "variables": {}, + }, ), ), ) diff --git a/convert2rhel/unit_tests/actions/pre_ponr_changes/handle_packages_test.py b/convert2rhel/unit_tests/actions/pre_ponr_changes/handle_packages_test.py index b04dec60b9..688f3db61e 100644 --- a/convert2rhel/unit_tests/actions/pre_ponr_changes/handle_packages_test.py +++ b/convert2rhel/unit_tests/actions/pre_ponr_changes/handle_packages_test.py @@ -18,14 +18,15 @@ import pytest import six -from convert2rhel import actions, pkghandler, pkgmanager, unit_tests +from convert2rhel import actions, pkghandler, pkgmanager, unit_tests, utils from convert2rhel.actions.pre_ponr_changes import handle_packages from convert2rhel.systeminfo import system_info from convert2rhel.unit_tests import ( FormatPkgInfoMocked, GetPackagesToRemoveMocked, GetThirdPartyPkgsMocked, - RemovePkgsUnlessFromRedhatMocked, + MockFunctionObject, + RemovePkgsMocked, ) from convert2rhel.unit_tests.conftest import centos8 @@ -34,6 +35,10 @@ from six.moves import mock +class RemovePkgsUnlessFromRedhatMocked(MockFunctionObject): + spec = handle_packages._remove_packages_unless_from_redhat + + @pytest.fixture def list_third_party_packages_instance(): return handle_packages.ListThirdPartyPackages() @@ -49,7 +54,7 @@ def test_list_third_party_packages_no_packages(list_third_party_packages_instanc @centos8 -def test_list_third_party_packages(pretend_os, list_third_party_packages_instance, monkeypatch, caplog): +def test_list_third_party_packages(pretend_os, list_third_party_packages_instance, monkeypatch): monkeypatch.setattr(pkghandler, "get_third_party_pkgs", GetThirdPartyPkgsMocked(pkg_selection="fingerprints")) monkeypatch.setattr(pkghandler, "format_pkg_info", FormatPkgInfoMocked(return_value=["shim", "ruby", "pytest"])) monkeypatch.setattr(system_info, "name", "Centos7") @@ -77,8 +82,8 @@ def test_list_third_party_packages(pretend_os, list_third_party_packages_instanc @pytest.fixture -def remove_excluded_packages_instance(): - return handle_packages.RemoveExcludedPackages() +def remove_special_packages_instance(): + return handle_packages.RemoveSpecialPackages() def get_centos_logos_pkg_object(): @@ -97,200 +102,170 @@ def get_centos_logos_pkg_object(): ) -def test_remove_excluded_packages_all_removed(remove_excluded_packages_instance, monkeypatch): - pkgs_to_remove = [get_centos_logos_pkg_object()] - pkgs_removed = ["centos-logos-70.0.6-3.el7.centos.noarch"] - expected = set( - ( - actions.ActionMessage( - level="INFO", - id="EXCLUDED_PACKAGES_REMOVED", - title="Excluded packages to be removed", - description="We have identified installed packages that match a pre-defined list of packages that are" - " to be removed during the conversion", - diagnosis="The following packages will be removed during the conversion: centos-logos-70.0.6-3.el7.centos.noarch", - remediations=None, +@pytest.fixture +def pkgs_to_remove(): + return [ + pkghandler.PackageInformation( + packager="CentOS BuildSystem ", + vendor="CentOS", + nevra=pkghandler.PackageNevra( + name="centos-logos", + epoch="0", + version="70.0.6", + release="3.el7.centos", + arch="noarch", ), - ) - ) - monkeypatch.setattr(system_info, "excluded_pkgs", ["installed_pkg", "not_installed_pkg"]) - monkeypatch.setattr(pkghandler, "get_packages_to_remove", GetPackagesToRemoveMocked(return_value=pkgs_to_remove)) - monkeypatch.setattr( - pkghandler, "remove_pkgs_unless_from_redhat", RemovePkgsUnlessFromRedhatMocked(return_value=pkgs_removed) - ) - - remove_excluded_packages_instance.run() - assert expected.issuperset(remove_excluded_packages_instance.messages) - assert expected.issubset(remove_excluded_packages_instance.messages) - assert pkghandler.get_packages_to_remove.call_count == 1 - assert pkghandler.remove_pkgs_unless_from_redhat.call_count == 1 - assert pkghandler.get_packages_to_remove.call_args == mock.call(system_info.excluded_pkgs) - assert remove_excluded_packages_instance.result.level == actions.STATUS_CODE["SUCCESS"] - - -@centos8 -def test_remove_excluded_packages_not_removed(pretend_os, remove_excluded_packages_instance, monkeypatch): - pkgs_removed = ["kernel-core"] - expected = set( - ( - actions.ActionMessage( - level="WARNING", - id="EXCLUDED_PACKAGES_NOT_REMOVED", - title="Excluded packages not removed", - description="Excluded packages which could not be removed", - diagnosis="The following packages were not removed: gpg-pubkey-1.0.0-1.x86_64, pkg1-None-None.None, pkg2-None-None.None", - remediations=None, + fingerprint="24c6a8a7f4a80eb5", + signature="RSA/SHA256, Wed Sep 30 20:10:39 2015, Key ID 24c6a8a7f4a80eb5", + ), + pkghandler.PackageInformation( + packager="CentOS BuildSystem ", + vendor="CentOS", + nevra=pkghandler.PackageNevra( + name="test1", + epoch="0", + version="1.0.6", + release="3.el7.centos", + arch="noarch", ), - actions.ActionMessage( - level="INFO", - id="EXCLUDED_PACKAGES_REMOVED", - title="Excluded packages to be removed", - description="We have identified installed packages that match a pre-defined list of packages that are" - " to be removed during the conversion", - diagnosis="The following packages will be removed during the conversion: kernel-core", - remediations=None, + fingerprint="24c6a8a7f4a80eb5", + signature="RSA/SHA256, Wed Sep 30 20:10:39 2015, Key ID 24c6a8a7f4a80eb5", + ), + pkghandler.PackageInformation( + packager="CentOS BuildSystem ", + vendor="CentOS", + nevra=pkghandler.PackageNevra( + name="test2", + epoch="0", + version="1.2.6", + release="3.el7.centos", + arch="noarch", ), - ) - ) - monkeypatch.setattr(system_info, "excluded_pkgs", ["installed_pkg", "not_installed_pkg"]) - monkeypatch.setattr(pkghandler, "get_packages_to_remove", GetPackagesToRemoveMocked(pkg_selection="fingerprints")) - monkeypatch.setattr( - pkghandler, "remove_pkgs_unless_from_redhat", RemovePkgsUnlessFromRedhatMocked(return_value=pkgs_removed) - ) - monkeypatch.setattr(pkgmanager, "TYPE", "dnf") - remove_excluded_packages_instance.run() - - assert expected.issuperset(remove_excluded_packages_instance.messages) - assert expected.issubset(remove_excluded_packages_instance.messages) - assert pkghandler.get_packages_to_remove.call_count == 1 - assert pkghandler.remove_pkgs_unless_from_redhat.call_count == 1 - assert pkghandler.get_packages_to_remove.call_args == mock.call(system_info.excluded_pkgs) - assert remove_excluded_packages_instance.result.level == actions.STATUS_CODE["SUCCESS"] - - -def test_remove_excluded_packages_error(remove_excluded_packages_instance, monkeypatch): - pkgs_removed = ["shim", "ruby", "kernel-core"] - monkeypatch.setattr(system_info, "excluded_pkgs", []) - monkeypatch.setattr(pkghandler, "get_packages_to_remove", GetPackagesToRemoveMocked(return_value=pkgs_removed)) - monkeypatch.setattr( - pkghandler, - "remove_pkgs_unless_from_redhat", - RemovePkgsUnlessFromRedhatMocked(side_effect=SystemExit("Raising SystemExit")), - ) - - remove_excluded_packages_instance.run() - - unit_tests.assert_actions_result( - remove_excluded_packages_instance, - level="ERROR", - id="EXCLUDED_PACKAGE_REMOVAL_FAILED", - title="Failed to remove excluded package", - description="The cause of this error is unknown, please look at the diagnosis for more information.", - diagnosis="Raising SystemExit", - ) - - -@pytest.fixture -def remove_repository_files_packages_instance(): - return handle_packages.RemoveRepositoryFilesPackages() + fingerprint="24c6a8a7f4a80eb5", + signature="RSA/SHA256, Wed Sep 30 20:10:39 2015, Key ID 24c6a8a7f4a80eb5", + ), + ] -def test_remove_repository_files_packages_all_removed(remove_repository_files_packages_instance, monkeypatch): - pkgs_to_remove = [get_centos_logos_pkg_object()] - pkgs_removed = [u"centos-logos-70.0.6-3.el7.centos.noarch"] - expected = set( - ( - actions.ActionMessage( - level="INFO", - id="REPOSITORY_FILE_PACKAGES_REMOVED", - title="Repository file packages to be removed", - description="We have identified installed packages that match a pre-defined list of packages that are" - " to be removed during the conversion", - diagnosis="The following packages will be removed during the conversion: centos-logos-70.0.6-3.el7.centos.noarch", - remediations=None, - ), +class TestRemoveSpecialPackages: + def test_dependency_order(self, remove_special_packages_instance): + expected_dependencies = ( + # We use the backed up repos in remove_pkgs_unless_from_redhat() + "BACKUP_REPOSITORY", + "BACKUP_PACKAGE_FILES", + "BACKUP_REDHAT_RELEASE", ) - ) - monkeypatch.setattr(system_info, "repofile_pkgs", ["installed_pkg", "not_installed_pkg"]) - monkeypatch.setattr(pkghandler, "get_packages_to_remove", GetPackagesToRemoveMocked(return_value=pkgs_to_remove)) - monkeypatch.setattr( - pkghandler, "remove_pkgs_unless_from_redhat", RemovePkgsUnlessFromRedhatMocked(return_value=pkgs_removed) - ) - - remove_repository_files_packages_instance.run() - - assert expected.issuperset(remove_repository_files_packages_instance.messages) - assert expected.issubset(remove_repository_files_packages_instance.messages) - assert pkghandler.get_packages_to_remove.call_count == 1 - assert pkghandler.remove_pkgs_unless_from_redhat.call_count == 1 - assert pkghandler.get_packages_to_remove.call_args == mock.call(system_info.repofile_pkgs) - assert remove_repository_files_packages_instance.result.level == actions.STATUS_CODE["SUCCESS"] - -@centos8 -def test_remove_repository_files_packages_not_removed( - pretend_os, remove_repository_files_packages_instance, monkeypatch -): - pkgs_removed = ["kernel-core"] - expected = set( - ( - actions.ActionMessage( - level="WARNING", - id="REPOSITORY_FILE_PACKAGES_NOT_REMOVED", - title="Repository file packages not removed", - description="Repository file packages which could not be removed", - diagnosis="The following packages were not removed: gpg-pubkey-1.0.0-1.x86_64, pkg1-None-None.None, pkg2-None-None.None", - remediations=None, - ), - actions.ActionMessage( - level="INFO", - id="REPOSITORY_FILE_PACKAGES_REMOVED", - title="Repository file packages to be removed", - description="We have identified installed packages that match a pre-defined list of packages that are" - " to be removed during the conversion", - diagnosis="The following packages will be removed during the conversion: kernel-core", - remediations=None, - ), + assert expected_dependencies == remove_special_packages_instance.dependencies + + def test_run_no_packages_to_remove(self, monkeypatch, remove_special_packages_instance, caplog): + monkeypatch.setattr(pkghandler, "get_packages_to_remove", GetPackagesToRemoveMocked(return_value=[])) + remove_special_packages_instance.run() + assert "No packages to backup and remove." in caplog.records[-1].message + + def test_run_all_removed(self, monkeypatch, remove_special_packages_instance): + pkgs_to_remove = [get_centos_logos_pkg_object()] + pkgs_removed = ["centos-logos-70.0.6-3.el7.centos.noarch"] + expected = set( + ( + actions.ActionMessage( + level="INFO", + id="SPECIAL_PACKAGES_REMOVED", + title="Special packages to be removed", + description="We have identified installed packages that match a pre-defined list of packages that are" + " to be removed during the conversion", + diagnosis="The following packages will be removed during the conversion: centos-logos-70.0.6-3.el7.centos.noarch", + remediations=None, + variables={}, + ), + ) + ) + monkeypatch.setattr( + pkghandler, "get_packages_to_remove", GetPackagesToRemoveMocked(return_value=pkgs_to_remove) + ) + monkeypatch.setattr( + handle_packages, + "_remove_packages_unless_from_redhat", + RemovePkgsUnlessFromRedhatMocked(return_value=pkgs_removed), ) - ) - monkeypatch.setattr(system_info, "repofile_pkgs", ["installed_pkg", "not_installed_pkg"]) - monkeypatch.setattr(pkghandler, "get_packages_to_remove", GetPackagesToRemoveMocked(pkg_selection="fingerprints")) - monkeypatch.setattr(pkgmanager, "TYPE", "dnf") - monkeypatch.setattr( - pkghandler, "remove_pkgs_unless_from_redhat", RemovePkgsUnlessFromRedhatMocked(return_value=pkgs_removed) - ) - - remove_repository_files_packages_instance.run() - - assert expected.issuperset(remove_repository_files_packages_instance.messages) - assert expected.issubset(remove_repository_files_packages_instance.messages) - assert pkghandler.get_packages_to_remove.call_count == 1 - assert pkghandler.remove_pkgs_unless_from_redhat.call_count == 1 - assert pkghandler.get_packages_to_remove.call_args == mock.call(system_info.repofile_pkgs) - assert remove_repository_files_packages_instance.result.level == actions.STATUS_CODE["SUCCESS"] - -def test_remove_repository_files_packages_dependency_order(remove_repository_files_packages_instance): - expected_dependencies = ("BACKUP_REDHAT_RELEASE", "BACKUP_REPOSITORY", "PRE_SUBSCRIPTION", "BACKUP_PACKAGE_FILES") + remove_special_packages_instance.run() + assert expected.issuperset(remove_special_packages_instance.messages) + assert expected.issubset(remove_special_packages_instance.messages) + assert pkghandler.get_packages_to_remove.call_count == 2 + assert handle_packages._remove_packages_unless_from_redhat.call_count == 1 + assert remove_special_packages_instance.result.level == actions.STATUS_CODE["SUCCESS"] + + @centos8 + def test_run_packages_not_removed(self, pretend_os, monkeypatch, remove_special_packages_instance): + pkgs_removed = ["kernel-core"] + expected = set( + ( + actions.ActionMessage( + level="WARNING", + id="SPECIAL_PACKAGES_NOT_REMOVED", + title="Special packages not removed", + description="Special packages which could not be removed", + diagnosis="The following packages were not removed: gpg-pubkey-1.0.0-1.x86_64, pkg1-None-None.None, pkg2-None-None.None", + remediations=None, + variables={}, + ), + actions.ActionMessage( + level="INFO", + id="SPECIAL_PACKAGES_REMOVED", + title="Special packages to be removed", + description=( + "We have identified installed packages that match a pre-defined list of packages that are" + " to be removed during the conversion" + ), + diagnosis="The following packages will be removed during the conversion: kernel-core", + remediations=None, + variables={}, + ), + ) + ) + monkeypatch.setattr( + pkghandler, "get_packages_to_remove", GetPackagesToRemoveMocked(pkg_selection="fingerprints") + ) + monkeypatch.setattr( + handle_packages, + "_remove_packages_unless_from_redhat", + RemovePkgsUnlessFromRedhatMocked(return_value=pkgs_removed), + ) + monkeypatch.setattr(pkgmanager, "TYPE", "dnf") + remove_special_packages_instance.run() + + assert expected.issuperset(remove_special_packages_instance.messages) + assert expected.issubset(remove_special_packages_instance.messages) + assert pkghandler.get_packages_to_remove.call_count == 2 + assert handle_packages._remove_packages_unless_from_redhat.call_count == 1 + assert remove_special_packages_instance.result.level == actions.STATUS_CODE["SUCCESS"] + + def test_run_packages_error(self, monkeypatch, remove_special_packages_instance): + monkeypatch.setattr( + pkghandler, "get_packages_to_remove", mock.Mock(side_effect=SystemExit("Raising SystemExit")) + ) + remove_special_packages_instance.run() + + unit_tests.assert_actions_result( + remove_special_packages_instance, + level="ERROR", + id="SPECIAL_PACKAGE_REMOVAL_FAILED", + title="Failed to remove some packages necessary for the conversion.", + description="The cause of this error is unknown, please look at the diagnosis for more information.", + diagnosis="Raising SystemExit", + ) - assert expected_dependencies == remove_repository_files_packages_instance.dependencies +def test_remove_packages_unless_from_redhat_no_pkgs(caplog): + assert not handle_packages._remove_packages_unless_from_redhat(pkgs_list=[]) + assert "\nNothing to do." in caplog.records[-1].message -def test_remove_repository_files_packages_error(remove_repository_files_packages_instance, monkeypatch): - monkeypatch.setattr(system_info, "repofile_pkgs", []) - monkeypatch.setattr( - pkghandler, - "remove_pkgs_unless_from_redhat", - RemovePkgsUnlessFromRedhatMocked(side_effect=SystemExit("Raising SystemExit")), - ) - remove_repository_files_packages_instance.run() +def test_remove_packages_unless_from_redhat(pkgs_to_remove, monkeypatch, caplog): + monkeypatch.setattr(utils, "remove_pkgs", RemovePkgsMocked()) + monkeypatch.setattr(pkghandler, "format_pkg_info", FormatPkgInfoMocked()) + handle_packages._remove_packages_unless_from_redhat(pkgs_list=pkgs_to_remove) - unit_tests.assert_actions_result( - remove_repository_files_packages_instance, - level="ERROR", - id="REPOSITORY_FILE_PACKAGE_REMOVAL_FAILED", - title="Repository file package removal failure", - description="The cause of this error is unknown, please look at the diagnosis for more information.", - diagnosis="Raising SystemExit", - ) + assert "Removing the following %s packages" % len(pkgs_to_remove) in caplog.records[-3].message + assert "Successfully removed %s packages" % len(pkgs_to_remove) in caplog.records[-1].message diff --git a/convert2rhel/unit_tests/actions/pre_ponr_changes/subscription_test.py b/convert2rhel/unit_tests/actions/pre_ponr_changes/subscription_test.py index 0f803c880e..6800649ac2 100644 --- a/convert2rhel/unit_tests/actions/pre_ponr_changes/subscription_test.py +++ b/convert2rhel/unit_tests/actions/pre_ponr_changes/subscription_test.py @@ -73,9 +73,9 @@ def test_run(self, monkeypatch, install_gpg_key_instance): class TestPreSubscription: def test_pre_subscription_dependency_order(self, pre_subscription_instance): expected_dependencies = ( + "REMOVE_SPECIAL_PACKAGES", "INSTALL_RED_HAT_CERT_FOR_YUM", "INSTALL_RED_HAT_GPG_KEY", - "REMOVE_EXCLUDED_PACKAGES", ) assert expected_dependencies == pre_subscription_instance.dependencies @@ -206,7 +206,10 @@ def test_pre_subscription_exceptions_with_remediations( class TestSubscribeSystem: def test_subscribe_system_dependency_order(self, subscribe_system_instance): - expected_dependencies = ("REMOVE_REPOSITORY_FILES_PACKAGES", "PRE_SUBSCRIPTION", "EUS_SYSTEM_CHECK") + expected_dependencies = ( + "PRE_SUBSCRIPTION", + "EUS_SYSTEM_CHECK", + ) assert expected_dependencies == subscribe_system_instance.dependencies diff --git a/convert2rhel/unit_tests/actions/pre_ponr_changes/transaction_test.py b/convert2rhel/unit_tests/actions/pre_ponr_changes/transaction_test.py index 19eb663e59..ab22c283b9 100644 --- a/convert2rhel/unit_tests/actions/pre_ponr_changes/transaction_test.py +++ b/convert2rhel/unit_tests/actions/pre_ponr_changes/transaction_test.py @@ -36,7 +36,6 @@ def validate_package_manager_transaction(): def test_validate_package_manager_transaction_dependency_order(validate_package_manager_transaction): expected_dependencies = ( "INSTALL_RED_HAT_GPG_KEY", - "REMOVE_EXCLUDED_PACKAGES", "REMOVE_IWLAX2XX_FIRMWARE", "CHECK_FIREWALLD_AVAILABILITY", "ENSURE_KERNEL_MODULES_COMPATIBILITY", diff --git a/convert2rhel/unit_tests/actions/report_test.py b/convert2rhel/unit_tests/actions/report_test.py index fa35ca0991..8b26d6ef08 100644 --- a/convert2rhel/unit_tests/actions/report_test.py +++ b/convert2rhel/unit_tests/actions/report_test.py @@ -41,16 +41,16 @@ ( { "CONVERT2RHEL_LATEST_VERSION": { - "result": dict(level=STATUS_CODE["SUCCESS"], id="SUCCESS"), + "result": {"level": STATUS_CODE["SUCCESS"], "id": "SUCCESS"}, "messages": [ - dict( - level=STATUS_CODE["WARNING"], - id="WARNING_ONE", - title="A warning message", - description="", - diagnosis="", - remediations="", - ), + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ONE", + "title": "A warning message", + "description": "", + "diagnosis": "", + "remediations": "", + }, ], }, }, @@ -59,16 +59,16 @@ "status": "WARNING", "actions": { "CONVERT2RHEL_LATEST_VERSION": { - "result": dict(level="SUCCESS", id="SUCCESS"), + "result": {"level": "SUCCESS", "id": "SUCCESS"}, "messages": [ - dict( - level="WARNING", - id="WARNING_ONE", - title="A warning message", - description="", - diagnosis="", - remediations="", - ), + { + "level": "WARNING", + "id": "WARNING_ONE", + "title": "A warning message", + "description": "", + "diagnosis": "", + "remediations": "", + }, ], }, }, @@ -77,16 +77,16 @@ ( { "CONVERT2RHEL_LATEST_VERSION": { - "result": dict(level=STATUS_CODE["SUCCESS"], id="SUCCESS"), + "result": {"level": STATUS_CODE["SUCCESS"], "id": "SUCCESS"}, "messages": [ - dict( - level=STATUS_CODE["WARNING"], - id="WARNING_ONE", - title="A warning message", - description="A description", - diagnosis="A diagnosis", - remediations="A remediations", - ), + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ONE", + "title": "A warning message", + "description": "A description", + "diagnosis": "A diagnosis", + "remediations": "A remediations", + }, ], }, }, @@ -95,16 +95,16 @@ "status": "WARNING", "actions": { "CONVERT2RHEL_LATEST_VERSION": { - "result": dict(level="SUCCESS", id="SUCCESS"), + "result": {"level": "SUCCESS", "id": "SUCCESS"}, "messages": [ - dict( - level="WARNING", - id="WARNING_ONE", - title="A warning message", - description="A description", - diagnosis="A diagnosis", - remediations="A remediations", - ), + { + "level": "WARNING", + "id": "WARNING_ONE", + "title": "A warning message", + "description": "A description", + "diagnosis": "A diagnosis", + "remediations": "A remediations", + }, ], }, }, @@ -131,8 +131,8 @@ def test_summary_as_json(results, expected, tmpdir): # parameter. ( { - "PreSubscription": dict( - messages=[ + "PreSubscription": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -143,7 +143,7 @@ def test_summary_as_json(results, expected, tmpdir): "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["SUCCESS"], "id": "SUCCESS", "title": "", @@ -152,7 +152,7 @@ def test_summary_as_json(results, expected, tmpdir): "remediations": "", "variables": {}, }, - ) + } }, True, [ @@ -162,9 +162,9 @@ def test_summary_as_json(results, expected, tmpdir): ), ( { - "PreSubscription": dict( - messages=[], - result={ + "PreSubscription": { + "messages": [], + "result": { "level": STATUS_CODE["SUCCESS"], "id": "SUCCESS", "title": "", @@ -173,9 +173,9 @@ def test_summary_as_json(results, expected, tmpdir): "remediations": "", "variables": {}, }, - ), - "PreSubscription2": dict( - messages=[ + }, + "PreSubscription2": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -186,7 +186,7 @@ def test_summary_as_json(results, expected, tmpdir): "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["SKIP"], "id": "SKIPPED", "title": "Skip", @@ -195,7 +195,7 @@ def test_summary_as_json(results, expected, tmpdir): "remediations": "move on", "variables": {}, }, - ), + }, }, True, [ @@ -208,9 +208,9 @@ def test_summary_as_json(results, expected, tmpdir): # the logs. ( { - "PreSubscription": dict( - messages=[], - result={ + "PreSubscription": { + "messages": [], + "result": { "level": STATUS_CODE["SUCCESS"], "id": "SUCCESS", "title": "", @@ -219,15 +219,15 @@ def test_summary_as_json(results, expected, tmpdir): "remediations": "", "variables": {}, }, - ) + } }, False, ["No problems detected during the analysis!"], ), ( { - "PreSubscription": dict( - messages=[ + "PreSubscription": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -238,7 +238,7 @@ def test_summary_as_json(results, expected, tmpdir): "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["SUCCESS"], "id": "SUCCESS", "title": "", @@ -247,7 +247,7 @@ def test_summary_as_json(results, expected, tmpdir): "remediations": "", "variables": {}, }, - ) + } }, False, [ @@ -256,8 +256,8 @@ def test_summary_as_json(results, expected, tmpdir): ), ( { - "PreSubscription": dict( - messages=[ + "PreSubscription": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -268,7 +268,7 @@ def test_summary_as_json(results, expected, tmpdir): "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["SUCCESS"], "id": "SUCCESS", "title": "", @@ -277,9 +277,9 @@ def test_summary_as_json(results, expected, tmpdir): "remediations": "", "variables": {}, }, - ), - "PreSubscription2": dict( - messages=[ + }, + "PreSubscription2": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -290,7 +290,7 @@ def test_summary_as_json(results, expected, tmpdir): "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["SKIP"], "id": "SKIPPED", "title": "Skip", @@ -299,7 +299,7 @@ def test_summary_as_json(results, expected, tmpdir): "remediations": "move on", "variables": {}, }, - ), + }, }, False, [ @@ -311,8 +311,8 @@ def test_summary_as_json(results, expected, tmpdir): # Test all messages are displayed, SKIP and higher ( { - "PreSubscription1": dict( - messages=[ + "PreSubscription1": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -323,7 +323,7 @@ def test_summary_as_json(results, expected, tmpdir): "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["SKIP"], "id": "SKIPPED", "title": "Skip", @@ -332,9 +332,9 @@ def test_summary_as_json(results, expected, tmpdir): "remediations": "move on", "variables": {}, }, - ), - "PreSubscription2": dict( - messages=[ + }, + "PreSubscription2": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -345,7 +345,7 @@ def test_summary_as_json(results, expected, tmpdir): "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["OVERRIDABLE"], "id": "OVERRIDABLE_ID", "title": "Overridable", @@ -354,7 +354,7 @@ def test_summary_as_json(results, expected, tmpdir): "remediations": "move on", "variables": {}, }, - ), + }, }, False, [ @@ -366,8 +366,8 @@ def test_summary_as_json(results, expected, tmpdir): ), ( { - "SkipAction": dict( - messages=[ + "SkipAction": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -378,7 +378,7 @@ def test_summary_as_json(results, expected, tmpdir): "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["SKIP"], "id": "SKIP", "title": "Skip", @@ -387,9 +387,9 @@ def test_summary_as_json(results, expected, tmpdir): "remediations": "move on", "variables": {}, }, - ), - "OverridableAction": dict( - messages=[ + }, + "OverridableAction": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -400,7 +400,7 @@ def test_summary_as_json(results, expected, tmpdir): "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["OVERRIDABLE"], "id": "OVERRIDABLE", "title": "Overridable", @@ -409,9 +409,9 @@ def test_summary_as_json(results, expected, tmpdir): "remediations": "move on", "variables": {}, }, - ), - "ErrorAction": dict( - messages=[ + }, + "ErrorAction": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -422,7 +422,7 @@ def test_summary_as_json(results, expected, tmpdir): "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["ERROR"], "id": "ERROR", "title": "Error", @@ -431,9 +431,9 @@ def test_summary_as_json(results, expected, tmpdir): "remediations": "move on", "variables": {}, }, - ), - "TestAction": dict( - messages=[ + }, + "TestAction": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -444,7 +444,7 @@ def test_summary_as_json(results, expected, tmpdir): "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["ERROR"], "id": "SECONDERROR", "title": "Error", @@ -453,7 +453,7 @@ def test_summary_as_json(results, expected, tmpdir): "remediations": "move on", "variables": {}, }, - ), + }, }, False, [ @@ -497,10 +497,10 @@ def test_results_summary_with_long_message(long_message, caplog): result.update(long_message) report.summary( { - "ErrorAction": dict( - messages=[], - result=result, - ) + "ErrorAction": { + "messages": [], + "result": result, + } }, disable_colors=True, ) @@ -541,9 +541,9 @@ def test_messages_summary_with_long_message(long_message, caplog): messages.update(long_message) report.summary( { - "ErrorAction": dict( - messages=[messages], - result={ + "ErrorAction": { + "messages": [messages], + "result": { "level": STATUS_CODE["SUCCESS"], "id": "", "title": "", @@ -552,7 +552,7 @@ def test_messages_summary_with_long_message(long_message, caplog): "remediations": "", "variables": {}, }, - ) + } }, disable_colors=True, ) @@ -578,9 +578,9 @@ def test_messages_summary_with_long_message(long_message, caplog): # Test all messages are displayed, SKIP and higher ( { - "PreSubscription2": dict( - messages=[], - result={ + "PreSubscription2": { + "messages": [], + "result": { "level": STATUS_CODE["SKIP"], "id": "SKIPPED", "title": "Skipped", @@ -589,10 +589,10 @@ def test_messages_summary_with_long_message(long_message, caplog): "remediations": "move on", "variables": {}, }, - ), - "PreSubscription1": dict( - messages=[], - result={ + }, + "PreSubscription1": { + "messages": [], + "result": { "level": STATUS_CODE["OVERRIDABLE"], "id": "SOME_OVERRIDABLE", "title": "Overridable", @@ -601,7 +601,7 @@ def test_messages_summary_with_long_message(long_message, caplog): "remediations": "move on", "variables": {}, }, - ), + }, }, False, [ @@ -611,9 +611,9 @@ def test_messages_summary_with_long_message(long_message, caplog): ), ( { - "SkipAction": dict( - messages=[], - result={ + "SkipAction": { + "messages": [], + "result": { "level": STATUS_CODE["SKIP"], "id": "SKIP", "title": "Skip", @@ -622,10 +622,10 @@ def test_messages_summary_with_long_message(long_message, caplog): "remediations": "move on", "variables": {}, }, - ), - "OverridableAction": dict( - messages=[], - result={ + }, + "OverridableAction": { + "messages": [], + "result": { "level": STATUS_CODE["OVERRIDABLE"], "id": "OVERRIDABLE", "title": "Overridable", @@ -634,10 +634,10 @@ def test_messages_summary_with_long_message(long_message, caplog): "remediations": "move on", "variables": {}, }, - ), - "ErrorAction": dict( - messages=[], - result={ + }, + "ErrorAction": { + "messages": [], + "result": { "level": STATUS_CODE["ERROR"], "id": "ERROR", "title": "Error", @@ -646,7 +646,7 @@ def test_messages_summary_with_long_message(long_message, caplog): "remediations": "move on", "variables": {}, }, - ), + }, }, False, [ @@ -658,9 +658,9 @@ def test_messages_summary_with_long_message(long_message, caplog): # Message order with `include_all_reports` set to True. ( { - "PreSubscription": dict( - messages=[], - result={ + "PreSubscription": { + "messages": [], + "result": { "level": STATUS_CODE["SUCCESS"], "id": "SUCCESS", "title": "", @@ -669,10 +669,10 @@ def test_messages_summary_with_long_message(long_message, caplog): "remediations": "", "variables": {}, }, - ), - "SkipAction": dict( - messages=[], - result={ + }, + "SkipAction": { + "messages": [], + "result": { "level": STATUS_CODE["SKIP"], "id": "SKIP", "title": "Skip", @@ -681,10 +681,10 @@ def test_messages_summary_with_long_message(long_message, caplog): "remediations": "move on", "variables": {}, }, - ), - "OverridableAction": dict( - messages=[], - result={ + }, + "OverridableAction": { + "messages": [], + "result": { "level": STATUS_CODE["OVERRIDABLE"], "id": "OVERRIDABLE", "title": "Overridable", @@ -693,10 +693,10 @@ def test_messages_summary_with_long_message(long_message, caplog): "remediations": "move on", "variables": {}, }, - ), - "ErrorAction": dict( - messages=[], - result={ + }, + "ErrorAction": { + "messages": [], + "result": { "level": STATUS_CODE["ERROR"], "id": "ERROR", "title": "Error", @@ -705,7 +705,7 @@ def test_messages_summary_with_long_message(long_message, caplog): "remediations": "move on", "variables": {}, }, - ), + }, }, True, [ @@ -737,8 +737,8 @@ def test_results_summary_ordering(results, include_all_reports, expected_results # Test all messages are displayed, SKIP and higher ( { - "PreSubscription2": dict( - messages=[ + "PreSubscription2": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -749,7 +749,7 @@ def test_results_summary_ordering(results, include_all_reports, expected_results "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["SKIP"], "id": "SKIPPED", "title": "Skip", @@ -758,10 +758,10 @@ def test_results_summary_ordering(results, include_all_reports, expected_results "remediations": "move on", "variables": {}, }, - ), - "PreSubscription1": dict( - messages=[], - result={ + }, + "PreSubscription1": { + "messages": [], + "result": { "level": STATUS_CODE["OVERRIDABLE"], "id": "SOME_OVERRIDABLE", "title": "Override", @@ -770,7 +770,7 @@ def test_results_summary_ordering(results, include_all_reports, expected_results "remediations": "move on", "variables": {}, }, - ), + }, }, False, [ @@ -781,8 +781,8 @@ def test_results_summary_ordering(results, include_all_reports, expected_results ), ( { - "SkipAction": dict( - messages=[ + "SkipAction": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -793,7 +793,7 @@ def test_results_summary_ordering(results, include_all_reports, expected_results "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["SKIP"], "id": "SKIP", "title": "Skip", @@ -802,9 +802,9 @@ def test_results_summary_ordering(results, include_all_reports, expected_results "remediations": "move on", "variables": {}, }, - ), - "OverridableAction": dict( - messages=[ + }, + "OverridableAction": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -815,7 +815,7 @@ def test_results_summary_ordering(results, include_all_reports, expected_results "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["OVERRIDABLE"], "id": "OVERRIDABLE", "title": "Overridable", @@ -824,10 +824,10 @@ def test_results_summary_ordering(results, include_all_reports, expected_results "remediations": "move on", "variables": {}, }, - ), - "ErrorAction": dict( - messages=[], - result={ + }, + "ErrorAction": { + "messages": [], + "result": { "level": STATUS_CODE["ERROR"], "id": "ERROR", "title": "Error", @@ -836,7 +836,7 @@ def test_results_summary_ordering(results, include_all_reports, expected_results "remediations": "move on", "variables": {}, }, - ), + }, }, False, [ @@ -850,8 +850,8 @@ def test_results_summary_ordering(results, include_all_reports, expected_results # Message order with `include_all_reports` set to True. ( { - "PreSubscription": dict( - messages=[ + "PreSubscription": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -862,7 +862,7 @@ def test_results_summary_ordering(results, include_all_reports, expected_results "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["SUCCESS"], "id": "SUCCESS", "title": "", @@ -871,9 +871,9 @@ def test_results_summary_ordering(results, include_all_reports, expected_results "remediations": "", "variables": {}, }, - ), - "SkipAction": dict( - messages=[ + }, + "SkipAction": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -884,7 +884,7 @@ def test_results_summary_ordering(results, include_all_reports, expected_results "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["SKIP"], "id": "SKIP", "title": "Skip", @@ -893,9 +893,9 @@ def test_results_summary_ordering(results, include_all_reports, expected_results "remediations": "move on", "variables": {}, }, - ), - "OverridableAction": dict( - messages=[ + }, + "OverridableAction": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -906,7 +906,7 @@ def test_results_summary_ordering(results, include_all_reports, expected_results "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["OVERRIDABLE"], "id": "OVERRIDABLE", "title": "Overridable", @@ -915,9 +915,9 @@ def test_results_summary_ordering(results, include_all_reports, expected_results "remediations": "move on", "variables": {}, }, - ), - "ErrorAction": dict( - messages=[ + }, + "ErrorAction": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -928,7 +928,7 @@ def test_results_summary_ordering(results, include_all_reports, expected_results "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["ERROR"], "id": "ERROR", "title": "Error", @@ -937,7 +937,7 @@ def test_results_summary_ordering(results, include_all_reports, expected_results "remediations": "move on", "variables": {}, }, - ), + }, }, True, [ @@ -973,8 +973,8 @@ def test_messages_summary_ordering(results, include_all_reports, expected_result ( ( { - "ErrorAction": dict( - messages=[ + "ErrorAction": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -985,7 +985,7 @@ def test_messages_summary_ordering(results, include_all_reports, expected_result "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["ERROR"], "id": "ERROR", "title": "Error", @@ -994,7 +994,7 @@ def test_messages_summary_ordering(results, include_all_reports, expected_result "remediations": "move on", "variables": {}, }, - ) + } }, "{begin}(ERROR) ErrorAction::ERROR - Error\n Description: Action error\n Diagnosis: User error\n Remediations: move on{end}".format( begin=bcolors.FAIL, end=bcolors.ENDC @@ -1005,8 +1005,8 @@ def test_messages_summary_ordering(results, include_all_reports, expected_result ), ( { - "OverridableAction": dict( - messages=[ + "OverridableAction": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -1017,7 +1017,7 @@ def test_messages_summary_ordering(results, include_all_reports, expected_result "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["OVERRIDABLE"], "id": "OVERRIDABLE", "title": "Overridable", @@ -1026,7 +1026,7 @@ def test_messages_summary_ordering(results, include_all_reports, expected_result "remediations": "move on", "variables": {}, }, - ) + } }, "{begin}(OVERRIDABLE) OverridableAction::OVERRIDABLE - Overridable\n Description: Action overridable\n Diagnosis: User overridable\n Remediations: move on{end}".format( begin=bcolors.FAIL, end=bcolors.ENDC @@ -1037,8 +1037,8 @@ def test_messages_summary_ordering(results, include_all_reports, expected_result ), ( { - "SkipAction": dict( - messages=[ + "SkipAction": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -1049,7 +1049,7 @@ def test_messages_summary_ordering(results, include_all_reports, expected_result "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["SKIP"], "id": "SKIP", "title": "Skip", @@ -1058,7 +1058,7 @@ def test_messages_summary_ordering(results, include_all_reports, expected_result "remediations": "move on", "variables": {}, }, - ) + } }, "{begin}(SKIP) SkipAction::SKIP - Skip\n Description: Action skip\n Diagnosis: User skip\n Remediations: move on{end}".format( begin=bcolors.FAIL, end=bcolors.ENDC @@ -1069,8 +1069,8 @@ def test_messages_summary_ordering(results, include_all_reports, expected_result ), ( { - "SuccessfulAction": dict( - messages=[ + "SuccessfulAction": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -1081,7 +1081,7 @@ def test_messages_summary_ordering(results, include_all_reports, expected_result "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["SUCCESS"], "id": "SUCCESS", "title": "", @@ -1090,7 +1090,7 @@ def test_messages_summary_ordering(results, include_all_reports, expected_result "remediations": "", "variables": {}, }, - ) + } }, "{begin}(SUCCESS) SuccessfulAction::SUCCESS - N/A{end}".format(begin=bcolors.OKGREEN, end=bcolors.ENDC), "{begin}(WARNING) SuccessfulAction::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediations: move on{end}".format( @@ -1110,8 +1110,8 @@ def test_summary_colors(results, expected_result, expected_message, caplog): ( ( { - "SkipAction": dict( - messages=[ + "SkipAction": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -1122,7 +1122,7 @@ def test_summary_colors(results, expected_result, expected_message, caplog): "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["SKIP"], "id": "SKIP", "title": "Skip", @@ -1131,9 +1131,9 @@ def test_summary_colors(results, expected_result, expected_message, caplog): "remediations": "move on", "variables": {}, }, - ), - "OverridableAction": dict( - messages=[ + }, + "OverridableAction": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -1144,7 +1144,7 @@ def test_summary_colors(results, expected_result, expected_message, caplog): "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["OVERRIDABLE"], "id": "OVERRIDABLE", "title": "Overridable", @@ -1153,9 +1153,9 @@ def test_summary_colors(results, expected_result, expected_message, caplog): "remediations": "move on", "variables": {}, }, - ), - "ErrorAction": dict( - messages=[ + }, + "ErrorAction": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -1166,7 +1166,7 @@ def test_summary_colors(results, expected_result, expected_message, caplog): "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["ERROR"], "id": "ERROR", "title": "Error", @@ -1175,9 +1175,9 @@ def test_summary_colors(results, expected_result, expected_message, caplog): "remediations": "move on", "variables": {}, }, - ), - "TestAction": dict( - messages=[ + }, + "TestAction": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -1188,7 +1188,7 @@ def test_summary_colors(results, expected_result, expected_message, caplog): "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["ERROR"], "id": "SECONDERROR", "title": "Error", @@ -1197,7 +1197,7 @@ def test_summary_colors(results, expected_result, expected_message, caplog): "remediations": "move on", "variables": {}, }, - ), + }, }, [ "{begin_fail}(ERROR) ErrorAction::ERROR - Error\n Description: Action error\n Diagnosis: User error\n Remediations: move on\n{end}", @@ -1232,8 +1232,8 @@ def test_summary_as_txt(results, text_lines, tmpdir, monkeypatch): ( ( { - "SkipAction": dict( - messages=[ + "SkipAction": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -1244,7 +1244,7 @@ def test_summary_as_txt(results, text_lines, tmpdir, monkeypatch): "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["SKIP"], "id": "SKIP", "title": "Skip", @@ -1253,9 +1253,9 @@ def test_summary_as_txt(results, text_lines, tmpdir, monkeypatch): "remediations": "move on", "variables": {}, }, - ), - "OverridableAction": dict( - messages=[ + }, + "OverridableAction": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -1266,7 +1266,7 @@ def test_summary_as_txt(results, text_lines, tmpdir, monkeypatch): "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["OVERRIDABLE"], "id": "OVERRIDABLE", "title": "Overridable", @@ -1275,9 +1275,9 @@ def test_summary_as_txt(results, text_lines, tmpdir, monkeypatch): "remediations": "move on", "variables": {}, }, - ), - "ErrorAction": dict( - messages=[ + }, + "ErrorAction": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -1288,7 +1288,7 @@ def test_summary_as_txt(results, text_lines, tmpdir, monkeypatch): "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["ERROR"], "id": "ERROR", "title": "Error", @@ -1297,9 +1297,9 @@ def test_summary_as_txt(results, text_lines, tmpdir, monkeypatch): "remediations": "move on", "variables": {}, }, - ), - "TestAction": dict( - messages=[ + }, + "TestAction": { + "messages": [ { "level": STATUS_CODE["WARNING"], "id": "WARNING_ID", @@ -1310,7 +1310,7 @@ def test_summary_as_txt(results, text_lines, tmpdir, monkeypatch): "variables": {}, } ], - result={ + "result": { "level": STATUS_CODE["ERROR"], "id": "SECONDERROR", "title": "Error", @@ -1319,7 +1319,7 @@ def test_summary_as_txt(results, text_lines, tmpdir, monkeypatch): "remediations": "move on", "variables": {}, }, - ), + }, }, [ "{begin_fail}(ERROR) ErrorAction::ERROR - Error\n Description: Action error\n Diagnosis: User error\n Remediations: move on\n{end}", diff --git a/convert2rhel/unit_tests/actions/system_checks/rhel_compatible_kernel_test.py b/convert2rhel/unit_tests/actions/system_checks/rhel_compatible_kernel_test.py index e7865cf719..2bd0622d27 100644 --- a/convert2rhel/unit_tests/actions/system_checks/rhel_compatible_kernel_test.py +++ b/convert2rhel/unit_tests/actions/system_checks/rhel_compatible_kernel_test.py @@ -50,7 +50,7 @@ def test_check_rhel_compatible_kernel_failure( rhel_compatible_kernel, "_bad_kernel_version", value=mock.Mock( - side_effect=KernelIncompatibleError("UNEXPECTED_VERSION", "Bad kernel version", dict(fake_data="fake")) + side_effect=KernelIncompatibleError("UNEXPECTED_VERSION", "Bad kernel version", {"fake_data": "fake"}) ), ) monkeypatch.setattr( @@ -154,7 +154,7 @@ def test_bad_kernel_version_success(kernel_release, major_ver, exp_return, monke None, "UNEXPECTED_VERSION", "Unexpected OS major version. Expected: {compatible_version}", - dict(compatible_version=COMPATIBLE_KERNELS_VERS.keys()), + {"compatible_version": COMPATIBLE_KERNELS_VERS.keys()}, ), ( "5.4.17-2102.200.13.el8uek.x86_64", @@ -162,7 +162,7 @@ def test_bad_kernel_version_success(kernel_release, major_ver, exp_return, monke "INCOMPATIBLE_VERSION", "Booted kernel version '{kernel_version}' does not correspond to the version " "'{compatible_version}' available in RHEL {rhel_major_version}", - dict(kernel_version="5.4.17", compatible_version=COMPATIBLE_KERNELS_VERS[8], rhel_major_version=8), + {"kernel_version": "5.4.17", "compatible_version": COMPATIBLE_KERNELS_VERS[8], "rhel_major_version": 8}, ), ), ) @@ -199,20 +199,20 @@ def test_bad_kernel_substring_success(kernel_release, exp_return): "INVALID_PACKAGE_SUBSTRING", "The booted kernel '{kernel_release}' contains one of the disallowed " "substrings: {bad_kernel_release_substrings}", - dict( - kernel_release="5.4.17-2102.200.13.el8uek.x86_64", - bad_kernel_release_substrings=BAD_KERNEL_RELEASE_SUBSTRINGS, - ), + { + "kernel_release": "5.4.17-2102.200.13.el8uek.x86_64", + "bad_kernel_release_substrings": BAD_KERNEL_RELEASE_SUBSTRINGS, + }, ), ( "3.10.0-514.2.2.rt56.424.el7.x86_64", "INVALID_PACKAGE_SUBSTRING", "The booted kernel '{kernel_release}' contains one of the disallowed " "substrings: {bad_kernel_release_substrings}", - dict( - kernel_release="3.10.0-514.2.2.rt56.424.el7.x86_64", - bad_kernel_release_substrings=BAD_KERNEL_RELEASE_SUBSTRINGS, - ), + { + "kernel_release": "3.10.0-514.2.2.rt56.424.el7.x86_64", + "bad_kernel_release_substrings": BAD_KERNEL_RELEASE_SUBSTRINGS, + }, ), ), ) @@ -285,7 +285,7 @@ def test_bad_kernel_package_signature_success( ), "INVALID_KERNEL_PACKAGE_SIGNATURE", "Custom kernel detected. The booted kernel needs to be signed by {os_vendor}.", - dict(os_vendor="CentOS"), + {"os_vendor": "CentOS"}, ), ), ) @@ -323,7 +323,7 @@ def test_bad_kernel_package_signature_invalid_signature( "UNSIGNED_PACKAGE", "The booted kernel {vmlinuz_path} is not owned by any installed package." " It needs to be owned by a package signed by {os_vendor}.", - dict(vmlinuz_path="/boot/vmlinuz-4.18.0-240.22.1.el8_3.x86_64", os_vendor="CentOS"), + {"vmlinuz_path": "/boot/vmlinuz-4.18.0-240.22.1.el8_3.x86_64", "os_vendor": "CentOS"}, ), ), ) diff --git a/convert2rhel/unit_tests/backup/backup_test.py b/convert2rhel/unit_tests/backup/backup_test.py index 3065d44409..883f59726b 100644 --- a/convert2rhel/unit_tests/backup/backup_test.py +++ b/convert2rhel/unit_tests/backup/backup_test.py @@ -1,297 +1,9 @@ __metaclass__ = type import pytest -import six - -from convert2rhel import backup, exceptions, repo, unit_tests -from convert2rhel.unit_tests import DownloadPkgMocked, ErrorOnRestoreRestorable, MinimalRestorable, RunSubprocessMocked -from convert2rhel.unit_tests.conftest import centos8 - - -six.add_move(six.MovedModule("mock", "mock", "unittest.mock")) -from six.moves import mock - - -class TestRemovePkgs: - def test_remove_pkgs_without_backup(self, monkeypatch): - monkeypatch.setattr(backup.changed_pkgs_control, "backup_and_track_removed_pkg", mock.Mock()) - monkeypatch.setattr(backup, "run_subprocess", RunSubprocessMocked()) - pkgs = ["pkg1", "pkg2", "pkg3"] - - backup.remove_pkgs(pkgs, False) - - assert backup.changed_pkgs_control.backup_and_track_removed_pkg.call_count == 0 - assert backup.run_subprocess.call_count == len(pkgs) - - rpm_remove_cmd = ["rpm", "-e", "--nodeps"] - for cmd, pkg in zip(backup.run_subprocess.cmds, pkgs): - assert rpm_remove_cmd + [pkg] == cmd - - def test_remove_pkgs_with_backup(self, monkeypatch): - monkeypatch.setattr(backup.changed_pkgs_control, "backup_and_track_removed_pkg", mock.Mock()) - monkeypatch.setattr(backup, "run_subprocess", RunSubprocessMocked()) - pkgs = ["pkg1", "pkg2", "pkg3"] - - backup.remove_pkgs(pkgs) - - assert backup.changed_pkgs_control.backup_and_track_removed_pkg.call_count == len(pkgs) - assert backup.run_subprocess.call_count == len(pkgs) - rpm_remove_cmd = ["rpm", "-e", "--nodeps"] - for cmd, pkg in zip(backup.run_subprocess.cmds, pkgs): - assert rpm_remove_cmd + [pkg] == cmd - - @pytest.mark.parametrize( - ("pkgs_to_remove", "ret_code", "backup_pkg", "critical", "expected"), - ( - (["pkg1"], 1, False, True, "Error: Couldn't remove {0}."), - (["pkg1"], 1, False, False, "Couldn't remove {0}."), - ), - ) - def test_remove_pkgs_failed_to_remove( - self, - pkgs_to_remove, - ret_code, - backup_pkg, - critical, - expected, - monkeypatch, - caplog, - ): - run_subprocess_mock = RunSubprocessMocked( - side_effect=unit_tests.run_subprocess_side_effect( - (("rpm", "-e", "--nodeps", pkgs_to_remove[0]), ("test", ret_code)), - ) - ) - monkeypatch.setattr( - backup, - "run_subprocess", - value=run_subprocess_mock, - ) - - if critical: - with pytest.raises(exceptions.CriticalError): - backup.remove_pkgs( - pkgs_to_remove=pkgs_to_remove, - backup=backup_pkg, - critical=critical, - ) - else: - backup.remove_pkgs(pkgs_to_remove=pkgs_to_remove, backup=backup_pkg, critical=critical) - - assert expected.format(pkgs_to_remove[0]) in caplog.records[-1].message - - def test_remove_pkgs_with_empty_list(self, caplog): - backup.remove_pkgs([]) - assert "No package to remove" in caplog.messages[-1] - - -class TestChangedPkgsControlInstallLocalRPMS: - def test_install_local_rpms_with_empty_list(self, monkeypatch): - monkeypatch.setattr(backup, "run_subprocess", RunSubprocessMocked()) - - backup.changed_pkgs_control._install_local_rpms([]) - - assert backup.run_subprocess.call_count == 0 - - def test_install_local_rpms_without_replace(self, monkeypatch): - monkeypatch.setattr(backup.changed_pkgs_control, "track_installed_pkg", mock.Mock()) - monkeypatch.setattr(backup, "run_subprocess", RunSubprocessMocked()) - pkgs = ["pkg1", "pkg2", "pkg3"] - - backup.changed_pkgs_control._install_local_rpms(pkgs) - - assert backup.changed_pkgs_control.track_installed_pkg.call_count == len(pkgs) - assert backup.run_subprocess.call_count == 1 - assert ["rpm", "-i", "pkg1", "pkg2", "pkg3"] == backup.run_subprocess.cmd - - def test_install_local_rpms_with_replace(self, monkeypatch): - monkeypatch.setattr(backup.changed_pkgs_control, "track_installed_pkg", mock.Mock()) - monkeypatch.setattr(backup, "run_subprocess", RunSubprocessMocked()) - pkgs = ["pkg1", "pkg2", "pkg3"] - - backup.changed_pkgs_control._install_local_rpms(pkgs, replace=True) - - assert backup.changed_pkgs_control.track_installed_pkg.call_count == len(pkgs) - assert backup.run_subprocess.call_count == 1 - assert ["rpm", "-i", "--replacepkgs", "pkg1", "pkg2", "pkg3"] == backup.run_subprocess.cmd - - -def test_backup_and_track_removed_pkg(monkeypatch): - monkeypatch.setattr(backup.RestorablePackage, "backup", mock.Mock()) - - control = backup.ChangedRPMPackagesController() - pkgs = ["pkg1", "pkg2", "pkg3"] - for pkg in pkgs: - control.backup_and_track_removed_pkg(pkg) - - assert backup.RestorablePackage.backup.call_count == len(pkgs) - assert len(control.removed_pkgs) == len(pkgs) - - -def test_track_installed_pkg(): - control = backup.ChangedRPMPackagesController() - pkgs = ["pkg1", "pkg2", "pkg3"] - for pkg in pkgs: - control.track_installed_pkg(pkg) - assert control.installed_pkgs == pkgs - - -def test_track_installed_pkgs(): - control = backup.ChangedRPMPackagesController() - pkgs = ["pkg1", "pkg2", "pkg3"] - control.track_installed_pkgs(pkgs) - assert control.installed_pkgs == pkgs - - -def test_changed_pkgs_control_remove_installed_pkgs(monkeypatch, caplog): - removed_pkgs = ["pkg_1"] - run_subprocess_mock = RunSubprocessMocked( - side_effect=unit_tests.run_subprocess_side_effect( - (("rpm", "-e", "--nodeps", removed_pkgs[0]), ("test", 0)), - ) - ) - monkeypatch.setattr( - backup, - "run_subprocess", - value=run_subprocess_mock, - ) - - control = backup.ChangedRPMPackagesController() - control.installed_pkgs = removed_pkgs - control._remove_installed_pkgs() - assert "Removing package: %s" % removed_pkgs[0] in caplog.records[-1].message - - -def test_changed_pkgs_control_install_removed_pkgs(monkeypatch): - install_local_rpms_mock = mock.Mock() - removed_pkgs = [mock.Mock()] - monkeypatch.setattr( - backup.changed_pkgs_control, - "_install_local_rpms", - value=install_local_rpms_mock, - ) - backup.changed_pkgs_control.removed_pkgs = removed_pkgs - backup.changed_pkgs_control._install_removed_pkgs() - assert install_local_rpms_mock.call_count == 1 - - -def test_changed_pkgs_control_install_removed_pkgs_without_path(monkeypatch, caplog): - install_local_rpms_mock = mock.Mock() - removed_pkgs = [mock.Mock()] - monkeypatch.setattr( - backup.changed_pkgs_control, - "_install_local_rpms", - value=install_local_rpms_mock, - ) - backup.changed_pkgs_control.removed_pkgs = removed_pkgs - backup.changed_pkgs_control.removed_pkgs[0].path = None - backup.changed_pkgs_control._install_removed_pkgs() - assert install_local_rpms_mock.call_count == 1 - assert "Couldn't find a backup" in caplog.records[-1].message - - -def test_changed_pkgs_control_restore_pkgs(monkeypatch): - install_local_rpms_mock = mock.Mock() - remove_pkgs_mock = mock.Mock() - monkeypatch.setattr( - backup.changed_pkgs_control, - "_install_local_rpms", - value=install_local_rpms_mock, - ) - monkeypatch.setattr(backup, "remove_pkgs", value=remove_pkgs_mock) - - backup.changed_pkgs_control.restore_pkgs() - assert install_local_rpms_mock.call_count == 1 - assert remove_pkgs_mock.call_count == 1 - - -@centos8 -def test_restorable_package_backup(pretend_os, monkeypatch, tmpdir): - backup_dir = str(tmpdir) - data_dir = str(tmpdir.join("data-dir")) - dowloaded_pkg_dir = str(tmpdir.join("some-path")) - download_pkg_mock = DownloadPkgMocked(return_value=dowloaded_pkg_dir) - monkeypatch.setattr(backup, "BACKUP_DIR", backup_dir) - monkeypatch.setattr(repo, "DATA_DIR", data_dir) - monkeypatch.setattr(backup, "download_pkg", download_pkg_mock) - rp = backup.RestorablePackage(pkgname="pkg-1") - rp.backup() - - assert download_pkg_mock.call_count == 1 - assert rp.path == dowloaded_pkg_dir - - -def test_restorable_package_backup_without_dir(monkeypatch, tmpdir, caplog): - backup_dir = str(tmpdir.join("non-existing")) - monkeypatch.setattr(backup, "BACKUP_DIR", backup_dir) - rp = backup.RestorablePackage(pkgname="pkg-1") - rp.backup() - - assert "Can't access %s" % backup_dir in caplog.records[-1].message - - -def test_changedrpms_packages_controller_install_local_rpms(monkeypatch, caplog): - pkgs = ["pkg-1"] - run_subprocess_mock = RunSubprocessMocked( - side_effect=unit_tests.run_subprocess_side_effect( - (("rpm", "-i", pkgs[0]), ("test", 1)), - ) - ) - monkeypatch.setattr( - backup, - "run_subprocess", - value=run_subprocess_mock, - ) - - control = backup.ChangedRPMPackagesController() - result = control._install_local_rpms(pkgs_to_install=pkgs, replace=False, critical=False) - - assert result == False - assert run_subprocess_mock.call_count == 1 - assert "Couldn't install %s packages." % pkgs[0] in caplog.records[-1].message - - -def test_changedrpms_packages_controller_install_local_rpms_system_exit(monkeypatch, caplog): - pkgs = ["pkg-1"] - run_subprocess_mock = RunSubprocessMocked( - side_effect=unit_tests.run_subprocess_side_effect( - (("rpm", "-i", pkgs[0]), ("test", 1)), - ) - ) - monkeypatch.setattr( - backup, - "run_subprocess", - value=run_subprocess_mock, - ) - - control = backup.ChangedRPMPackagesController() - with pytest.raises(exceptions.CriticalError): - control._install_local_rpms(pkgs_to_install=pkgs, replace=False, critical=True) - - assert run_subprocess_mock.call_count == 1 - assert "Error: Couldn't install %s packages." % pkgs[0] in caplog.records[-1].message - - -@pytest.mark.parametrize( - ("is_eus_system", "has_internet_access"), - ((True, True), (False, False), (True, False), (False, True)), -) -@centos8 -def test_restorable_package_backup(pretend_os, is_eus_system, has_internet_access, tmpdir, monkeypatch): - pkg_to_backup = "pkg-1" - - # Python 2.7 needs a string or buffer and not a LocalPath - tmpdir = str(tmpdir) - download_pkg_mock = DownloadPkgMocked() - monkeypatch.setattr(backup, "download_pkg", value=download_pkg_mock) - monkeypatch.setattr(backup, "BACKUP_DIR", value=tmpdir) - monkeypatch.setattr(backup.system_info, "corresponds_to_rhel_eus_release", value=lambda: is_eus_system) - monkeypatch.setattr(backup, "get_hardcoded_repofiles_dir", value=lambda: tmpdir if is_eus_system else None) - backup.system_info.has_internet_access = has_internet_access - - rp = backup.RestorablePackage(pkgname=pkg_to_backup) - rp.backup() - assert download_pkg_mock.call_count == 1 + +from convert2rhel import backup +from convert2rhel.unit_tests import ErrorOnRestoreRestorable, MinimalRestorable @pytest.fixture @@ -441,16 +153,3 @@ def test_pop_to_partition(self, backup_controller): assert backup_controller._restorables == [] # End of tests that are for the 1.4 partition hack. - - -@pytest.mark.parametrize( - ("pkg_nevra", "nvra_without_epoch"), - ( - ("7:oraclelinux-release-7.9-1.0.9.el7.x86_64", "oraclelinux-release-7.9-1.0.9.el7.x86_64"), - ("oraclelinux-release-8:8.2-1.0.8.el8.x86_64", "oraclelinux-release-8:8.2-1.0.8.el8.x86_64"), - ("1:mod_proxy_html-2.4.6-97.el7.centos.5.x86_64", "mod_proxy_html-2.4.6-97.el7.centos.5.x86_64"), - ("httpd-tools-2.4.6-97.el7.centos.5.x86_64", "httpd-tools-2.4.6-97.el7.centos.5.x86_64"), - ), -) -def test_remove_epoch_from_yum_nevra_notation(pkg_nevra, nvra_without_epoch): - assert backup.remove_epoch_from_yum_nevra_notation(pkg_nevra) == nvra_without_epoch diff --git a/convert2rhel/unit_tests/backup/packages_test.py b/convert2rhel/unit_tests/backup/packages_test.py index 9096d166a0..a8c4f92b72 100644 --- a/convert2rhel/unit_tests/backup/packages_test.py +++ b/convert2rhel/unit_tests/backup/packages_test.py @@ -22,9 +22,9 @@ import pytest import six -from convert2rhel import exceptions, pkghandler, utils +from convert2rhel import exceptions, pkghandler, pkgmanager, unit_tests, utils from convert2rhel.backup import packages -from convert2rhel.backup.packages import RestorablePackageSet +from convert2rhel.backup.packages import RestorablePackage, RestorablePackageSet from convert2rhel.systeminfo import Version from convert2rhel.unit_tests import ( CallYumCmdMocked, @@ -32,9 +32,10 @@ GetInstalledPkgInformationMocked, MockFunctionObject, RemovePkgsMocked, + RunSubprocessMocked, StoreContentToFileMocked, ) -from convert2rhel.unit_tests.conftest import centos8 +from convert2rhel.unit_tests.conftest import centos7 six.add_move(six.MovedModule("mock", "mock", "unittest.mock")) @@ -66,6 +67,195 @@ def __call__(self, pkgs, dest, *args, **kwargs): return super(DownloadPkgsMocked, self).__call__(pkgs, dest, *args, **kwargs) +class TestRestorablePackage: + def test_install_local_rpms_with_empty_list(self, monkeypatch): + monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked()) + + rp = RestorablePackage(pkgs=["test.rpm"]) + rp._backedup_pkgs_paths = ["test.rpm"] + + assert rp._install_local_rpms() + assert utils.run_subprocess.call_count == 1 + assert ["rpm", "-i", "test.rpm"] == utils.run_subprocess.cmd + + def test_install_local_rpms_with_replace(self, monkeypatch): + monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked()) + + rp = RestorablePackage(pkgs=["test.rpm"]) + rp._backedup_pkgs_paths = ["test.rpm"] + + assert rp._install_local_rpms(replace=True) + assert utils.run_subprocess.call_count == 1 + assert ["rpm", "-i", "--replacepkgs", "test.rpm"] == utils.run_subprocess.cmd + + def test_install_local_rpms_without_path(self, caplog): + rp = RestorablePackage(pkgs=["test.rpm"]) + assert not rp._install_local_rpms() + assert "No package to install." in caplog.records[-1].message + + def test_enable(self, monkeypatch, tmpdir, global_backup_control): + monkeypatch.setattr(packages, "BACKUP_DIR", str(tmpdir)) + monkeypatch.setattr(utils, "download_pkg", DownloadPkgMocked()) + pkgs = ["pkg1", "pkg2", "pkg3"] + rp = RestorablePackage(pkgs=pkgs) + global_backup_control.push(rp) + + assert utils.download_pkg.call_count == len(pkgs) + assert len(global_backup_control._restorables) == 1 + assert len(rp._backedup_pkgs_paths) == len(pkgs) + + def test_enable_eus_systems(self, monkeypatch, tmpdir, global_system_info): + monkeypatch.setattr(packages, "BACKUP_DIR", str(tmpdir)) + monkeypatch.setattr(packages, "get_hardcoded_repofiles_dir", mock.Mock(return_value=str(tmpdir))) + monkeypatch.setattr(utils, "download_pkg", DownloadPkgMocked()) + monkeypatch.setattr(packages, "system_info", global_system_info) + + packages.system_info.eus_system = True + packages.system_info.id = "centos" + + rp = RestorablePackage(pkgs=["test.rpm"]) + rp._backedup_pkgs_paths = ["test.rpm"] + rp.enable() + + assert packages.get_hardcoded_repofiles_dir.call_count == 1 + assert utils.download_pkg.call_count == 1 + + @pytest.mark.parametrize( + ( + "has_internet_access", + "expected", + ), + ( + ( + True, + "Using repository files stored in %s", + ), + ( + False, + "Not using repository files stored in %s due to the absence of internet access.", + ), + ), + ) + def test_enable_has_internet_connection( + self, has_internet_access, expected, monkeypatch, tmpdir, global_system_info, caplog + ): + tmpdir = str(tmpdir) + monkeypatch.setattr(packages, "BACKUP_DIR", tmpdir) + monkeypatch.setattr(utils, "download_pkg", DownloadPkgMocked()) + monkeypatch.setattr(packages, "system_info", global_system_info) + + global_system_info.has_internet_access = has_internet_access + + rp = RestorablePackage(pkgs=["test.rpm"], reposdir=tmpdir) + rp._backedup_pkgs_paths = ["test.rpm"] + rp.enable() + + assert utils.download_pkg.call_count == 1 + assert expected % tmpdir in caplog.records[-1].message + + def test_package_already_enabled(self, monkeypatch, tmpdir): + monkeypatch.setattr(packages, "BACKUP_DIR", str(tmpdir)) + monkeypatch.setattr(utils, "download_pkg", DownloadPkgMocked()) + + rp = RestorablePackage(pkgs=["test.rpm"]) + rp.enable() + assert utils.download_pkg.call_count == 1 + + rp.enable() + # Assert that we are still at call_count 1 meaning that we returning + # earlier without going through the backup. + assert utils.download_pkg.call_count == 1 + + def test_restore(self, monkeypatch): + monkeypatch.setattr( + packages.RestorablePackage, + "_install_local_rpms", + value=mock.Mock(), + ) + monkeypatch.setattr(utils, "remove_orphan_folders", value=mock.Mock()) + + rp = RestorablePackage(pkgs=["test.rpm"]) + rp.enabled = True + rp._backedup_pkgs_paths = ["test.rpm"] + rp.restore() + assert utils.remove_orphan_folders.call_count == 1 + assert rp._install_local_rpms.call_count == 1 + + def test_restore_pkg_without_path(self, monkeypatch, caplog): + monkeypatch.setattr(utils, "remove_orphan_folders", value=mock.Mock()) + + rp = RestorablePackage(pkgs=["test.rpm"]) + rp.enabled = True + rp.restore() + assert utils.remove_orphan_folders.call_count == 1 + assert "Couldn't find a backup for test.rpm package." in caplog.records[-1].message + + def test_restore_second_restore(self, monkeypatch): + monkeypatch.setattr( + packages.RestorablePackage, + "_install_local_rpms", + value=mock.Mock(), + ) + monkeypatch.setattr(utils, "remove_orphan_folders", value=mock.Mock()) + + rp = RestorablePackage(pkgs=["test.rpm"]) + rp.enabled = True + rp._backedup_pkgs_paths = ["test.rpm"] + rp.restore() + assert utils.remove_orphan_folders.call_count == 1 + assert rp._install_local_rpms.call_count == 1 + + rp.restore() + assert utils.remove_orphan_folders.call_count == 1 + assert rp._install_local_rpms.call_count == 1 + + def test_restorable_package_backup_without_dir(self, monkeypatch, tmpdir, caplog): + backup_dir = str(tmpdir.join("non-existing")) + monkeypatch.setattr(packages, "BACKUP_DIR", backup_dir) + rp = RestorablePackage(pkgs=["pkg-1"]) + rp.enable() + + assert "Can't access %s" % backup_dir in caplog.records[-1].message + + def test_install_local_rpms_package_install_warning(self, monkeypatch, caplog): + pkg_name = "pkg-1" + run_subprocess_mock = RunSubprocessMocked( + side_effect=unit_tests.run_subprocess_side_effect( + (("rpm", "-i", pkg_name), ("test", 1)), + ) + ) + monkeypatch.setattr(utils, "run_subprocess", value=run_subprocess_mock) + + rp = RestorablePackage(pkgs=[pkg_name]) + rp._backedup_pkgs_paths = pkg_name + result = rp._install_local_rpms(replace=False, critical=False) + + assert result == False + assert run_subprocess_mock.call_count == 1 + assert "Couldn't install %s packages." % pkg_name in caplog.records[-1].message + + def test_test_install_local_rpms_system_exit(self, monkeypatch, caplog): + pkg_name = "pkg-1" + run_subprocess_mock = RunSubprocessMocked( + side_effect=unit_tests.run_subprocess_side_effect( + (("rpm", "-i", pkg_name), ("test", 1)), + ) + ) + monkeypatch.setattr( + utils, + "run_subprocess", + value=run_subprocess_mock, + ) + + rp = RestorablePackage(pkgs=[pkg_name]) + rp._backedup_pkgs_paths = pkg_name + with pytest.raises(exceptions.CriticalError): + rp._install_local_rpms(replace=False, critical=True) + + assert run_subprocess_mock.call_count == 1 + assert "Error: Couldn't install %s packages." % pkg_name in caplog.records[-1].message + + class TestRestorablePackageSet: @staticmethod def fake_download_pkg(pkg, *args, **kwargs): @@ -105,26 +295,40 @@ def package_set(self, monkeypatch, tmpdir): return RestorablePackageSet(["subscription-manager", "python-syspurpose"]) - def test_smoketest_init(self): - package_set = RestorablePackageSet(["pkg1"]) + @pytest.mark.parametrize( + ("pkgs_to_install", "pkgs_to_update", "reposdir"), + ( + (["pkg-1"], [], None), + (["pkg-1"], [], "test-dir"), + ([], ["pkg-1"], None), + ([], [], "test-dir"), + (["pkg-1"], ["pkg-2"], None), + ), + ) + def test_smoketest_init(self, pkgs_to_install, pkgs_to_update, reposdir): + package_set = RestorablePackageSet(pkgs_to_install, pkgs_to_update, reposdir) + + assert package_set.pkgs_to_install == pkgs_to_install + assert package_set.pkgs_to_update == pkgs_to_update + assert package_set.reposdir == reposdir - assert package_set.pkgs_to_install == ["pkg1"] assert package_set.enabled is False # We actually care that this is an empty list and not just False-y assert package_set.installed_pkgs == [] # pylint: disable=use-implicit-booleaness-not-comparison @pytest.mark.parametrize( - ("rhel_major_version"), + ("major", "minor"), ( (7, 10), (8, 5), (9, 3), ), ) - def test_enable_need_to_install(self, rhel_major_version, package_set, global_system_info, caplog, monkeypatch): - global_system_info.version = Version(*rhel_major_version) + def test_enable_need_to_install(self, major, minor, package_set, global_system_info, caplog, monkeypatch, tmpdir): + repofile = tmpdir.join("repofile.repo") + global_system_info.version = Version(major, minor) monkeypatch.setattr(packages, "system_info", global_system_info) - + monkeypatch.setattr(packages, "_UBI_REPO_MAPPING", {major: (str(repofile), "test")}) monkeypatch.setattr(utils, "download_pkg", DownloadPkgMocked(side_effect=self.fake_download_pkg)) monkeypatch.setattr(packages, "call_yum_cmd", CallYumCmdMocked()) monkeypatch.setattr(utils, "get_package_name_from_rpm", self.fake_get_pkg_name_from_rpm) @@ -143,19 +347,17 @@ def test_enable_need_to_install(self, rhel_major_version, package_set, global_sy assert "json-c" not in package_set.installed_pkgs assert "json-c.x86_64" not in package_set.installed_pkgs - @centos8 - def test_enable_call_yum_cmd_fail(self, pretend_os, package_set, global_system_info, caplog, monkeypatch): - global_system_info.version = Version(7, 0) - monkeypatch.setattr(packages, "system_info", global_system_info) + @centos7 + def test_enable_call_yum_cmd_fail(self, pretend_os, package_set, caplog, monkeypatch, tmpdir): + repofile = tmpdir.join("repofile.repo") monkeypatch.setattr( pkghandler, "get_installed_pkg_information", GetInstalledPkgInformationMocked(side_effect=(["subscription-manager"], [], [])), ) monkeypatch.setattr(utils, "download_pkg", DownloadPkgMocked(side_effect=self.fake_download_pkg)) - - yum_cmd = CallYumCmdMocked(return_code=1) - monkeypatch.setattr(pkghandler, "call_yum_cmd", yum_cmd) + monkeypatch.setattr(packages, "_UBI_REPO_MAPPING", {7: (str(repofile), "test")}) + monkeypatch.setattr(pkgmanager, "call_yum_cmd", CallYumCmdMocked(return_code=1)) monkeypatch.setattr(utils, "get_package_name_from_rpm", self.fake_get_pkg_name_from_rpm) with pytest.raises(exceptions.CriticalError): @@ -189,14 +391,14 @@ def test_enable_no_packages(self, package_set, caplog, monkeypatch, global_syste def test_restore(self, package_set, monkeypatch): mock_remove_pkgs = RemovePkgsMocked() - monkeypatch.setattr(packages, "remove_pkgs", mock_remove_pkgs) + monkeypatch.setattr(utils, "remove_pkgs", mock_remove_pkgs) package_set.enabled = 1 package_set.installed_pkgs = ["one", "two"] package_set.restore() assert mock_remove_pkgs.call_count == 1 - mock_remove_pkgs.assert_called_with(["one", "two"], backup=False, critical=False) + mock_remove_pkgs.assert_called_with(["one", "two"], critical=False) @pytest.mark.parametrize( ("install", "update", "removed"), @@ -230,7 +432,7 @@ def test_restore(self, package_set, monkeypatch): ) def test_restore_with_pkgs_in_updates(self, install, update, removed, package_set, monkeypatch): remove_pkgs_mock = RemovePkgsMocked() - monkeypatch.setattr(packages, "remove_pkgs", remove_pkgs_mock) + monkeypatch.setattr(utils, "remove_pkgs", remove_pkgs_mock) package_set.enabled = 1 package_set.installed_pkgs = install @@ -238,11 +440,11 @@ def test_restore_with_pkgs_in_updates(self, install, update, removed, package_se package_set.restore() - remove_pkgs_mock.assert_called_with(removed, backup=False, critical=False) + remove_pkgs_mock.assert_called_with(removed, critical=False) def test_restore_not_enabled(self, package_set, monkeypatch): mock_remove_pkgs = RemovePkgsMocked() - monkeypatch.setattr(packages, "remove_pkgs", mock_remove_pkgs) + monkeypatch.setattr(utils, "remove_pkgs", mock_remove_pkgs) package_set.enabled = 1 package_set.restore() diff --git a/convert2rhel/unit_tests/conftest.py b/convert2rhel/unit_tests/conftest.py index e71ee9e580..d6ca144dae 100644 --- a/convert2rhel/unit_tests/conftest.py +++ b/convert2rhel/unit_tests/conftest.py @@ -82,7 +82,7 @@ def setup_logger(tmpdir, request): @pytest.fixture -def system_cert_with_target_path(monkeypatch, tmpdir, request): +def system_cert_with_target_path(tmpdir): """ Create a single RestorablePEMCert backed by a temp file. @@ -227,6 +227,7 @@ def pretend_os(request, pkg_root, monkeypatch): "_check_internet_access", value=lambda: True, ) + monkeypatch.setattr(system_info, "releasever", value=system_version_major) system_info.resolve_system_info() diff --git a/convert2rhel/unit_tests/main_test.py b/convert2rhel/unit_tests/main_test.py index 873c94a680..26e0571b37 100644 --- a/convert2rhel/unit_tests/main_test.py +++ b/convert2rhel/unit_tests/main_test.py @@ -53,16 +53,11 @@ class TestRollbackChanges: - @pytest.fixture(autouse=True) - def mock_rollback_functions(self, monkeypatch): - monkeypatch.setattr(backup.changed_pkgs_control, "restore_pkgs", mock.Mock()) - def test_rollback_changes(self, monkeypatch, global_backup_control): monkeypatch.setattr(global_backup_control, "pop_all", mock.Mock()) main.rollback_changes() - assert backup.changed_pkgs_control.restore_pkgs.call_count == 1 # Note: when we remove the BackupController partition hack, the first # of these calls will go away assert global_backup_control.pop_all.call_args_list == [mock.call(_honor_partitions=True), mock.call()] diff --git a/convert2rhel/unit_tests/pkghandler_test.py b/convert2rhel/unit_tests/pkghandler_test.py index 35e731daca..3b9d490ac2 100644 --- a/convert2rhel/unit_tests/pkghandler_test.py +++ b/convert2rhel/unit_tests/pkghandler_test.py @@ -27,7 +27,7 @@ import rpm import six -from convert2rhel import backup, pkghandler, pkgmanager, unit_tests, utils +from convert2rhel import pkghandler, pkgmanager, unit_tests, utils from convert2rhel.backup.certs import RestorableRpmKey from convert2rhel.backup.files import RestorableFile from convert2rhel.pkghandler import ( @@ -180,15 +180,15 @@ def test_clear_versionlock_user_says_yes(self, monkeypatch, global_backup_contro monkeypatch.setattr(utils, "ask_to_continue", mock.Mock()) monkeypatch.setattr(os.path, "isfile", mock.Mock(return_value=True)) monkeypatch.setattr(os.path, "getsize", mock.Mock(return_value=1)) - monkeypatch.setattr(pkghandler, "call_yum_cmd", CallYumCmdMocked()) + monkeypatch.setattr(pkgmanager, "call_yum_cmd", CallYumCmdMocked()) monkeypatch.setattr(RestorableFile, "enable", mock.Mock()) monkeypatch.setattr(RestorableFile, "restore", mock.Mock()) pkghandler.clear_versionlock() - assert pkghandler.call_yum_cmd.call_count == 1 - assert pkghandler.call_yum_cmd.command == "versionlock" - assert pkghandler.call_yum_cmd.args == ["clear"] + assert pkgmanager.call_yum_cmd.call_count == 1 + assert pkgmanager.call_yum_cmd.command == "versionlock" + assert pkgmanager.call_yum_cmd.args == ["clear"] assert len(global_backup_control._restorables) == 1 def test_clear_versionlock_user_says_no(self, monkeypatch): @@ -197,94 +197,12 @@ def test_clear_versionlock_user_says_no(self, monkeypatch): ) monkeypatch.setattr(os.path, "isfile", mock.Mock(return_value=True)) monkeypatch.setattr(os.path, "getsize", mock.Mock(return_value=1)) - monkeypatch.setattr(pkghandler, "call_yum_cmd", CallYumCmdMocked()) + monkeypatch.setattr(pkgmanager, "call_yum_cmd", CallYumCmdMocked()) with pytest.raises(SystemExit): pkghandler.clear_versionlock() - assert not pkghandler.call_yum_cmd.called - - -class TestCallYumCmd: - def test_call_yum_cmd(self, monkeypatch): - monkeypatch.setattr(system_info, "version", Version(8, 0)) - monkeypatch.setattr(system_info, "releasever", "8") - monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked()) - - pkghandler.call_yum_cmd("install") - - assert utils.run_subprocess.cmd == [ - "yum", - "install", - "-y", - "--releasever=8", - "--setopt=module_platform_id=platform:el8", - ] - - def test_call_yum_cmd_not_setting_releasever(self, monkeypatch): - monkeypatch.setattr(system_info, "version", Version(7, 0)) - monkeypatch.setattr(system_info, "releasever", "7Server") - monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked()) - - pkghandler.call_yum_cmd("install", set_releasever=False) - - assert utils.run_subprocess.cmd == ["yum", "install", "-y"] - - def test_call_yum_cmd_with_disablerepo_and_enablerepo(self, monkeypatch): - monkeypatch.setattr(system_info, "version", Version(7, 0)) - monkeypatch.setattr(system_info, "releasever", None) - monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked()) - monkeypatch.setattr(tool_opts, "no_rhsm", True) - monkeypatch.setattr(tool_opts, "disablerepo", ["*"]) - monkeypatch.setattr(tool_opts, "enablerepo", ["rhel-7-extras-rpm"]) - - pkghandler.call_yum_cmd("install") - - assert utils.run_subprocess.cmd == [ - "yum", - "install", - "-y", - "--disablerepo=*", - "--enablerepo=rhel-7-extras-rpm", - ] - - def test_call_yum_cmd_with_submgr_enabled_repos(self, monkeypatch): - monkeypatch.setattr(system_info, "version", Version(7, 0)) - monkeypatch.setattr(system_info, "releasever", None) - monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked()) - monkeypatch.setattr(system_info, "submgr_enabled_repos", ["rhel-7-extras-rpm"]) - monkeypatch.setattr(tool_opts, "enablerepo", ["not-to-be-used-in-the-yum-call"]) - - pkghandler.call_yum_cmd("install") - - assert utils.run_subprocess.cmd == ["yum", "install", "-y", "--enablerepo=rhel-7-extras-rpm"] - - def test_call_yum_cmd_with_repo_overrides(self, monkeypatch): - monkeypatch.setattr(system_info, "version", Version(7, 0)) - monkeypatch.setattr(system_info, "releasever", None) - monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked()) - monkeypatch.setattr(system_info, "submgr_enabled_repos", ["not-to-be-used-in-the-yum-call"]) - monkeypatch.setattr(tool_opts, "enablerepo", ["not-to-be-used-in-the-yum-call"]) - - pkghandler.call_yum_cmd("install", ["pkg"], enable_repos=[], disable_repos=[]) - - assert utils.run_subprocess.cmd == ["yum", "install", "-y", "pkg"] - - pkghandler.call_yum_cmd( - "install", - ["pkg"], - enable_repos=["enable-repo"], - disable_repos=["disable-repo"], - ) - - assert utils.run_subprocess.cmd == [ - "yum", - "install", - "-y", - "--disablerepo=disable-repo", - "--enablerepo=enable-repo", - "pkg", - ] + assert not pkgmanager.call_yum_cmd.called class TestGetRpmHeader: @@ -314,9 +232,8 @@ def test_get_rpm_header_failure(self, monkeypatch): class TestPreserveOnlyRHELKernel: - def test_preserve_only_rhel_kernel(self, monkeypatch): - monkeypatch.setattr(system_info, "version", Version(7, 0)) - monkeypatch.setattr(system_info, "releasever", None) + @centos7 + def test_preserve_only_rhel_kernel(self, pretend_os, monkeypatch): monkeypatch.setattr(pkghandler, "install_rhel_kernel", lambda: True) monkeypatch.setattr(pkghandler, "fix_invalid_grub2_entries", lambda: None) monkeypatch.setattr(pkghandler, "remove_non_rhel_kernels", mock.Mock(return_value=[])) @@ -333,7 +250,7 @@ def test_preserve_only_rhel_kernel(self, monkeypatch): pkghandler.preserve_only_rhel_kernel() - assert utils.run_subprocess.cmd == ["yum", "update", "-y", "kernel"] + assert utils.run_subprocess.cmd == ["yum", "update", "-y", "--releasever=7Server", "kernel"] assert pkghandler.get_installed_pkgs_by_fingerprint.call_count == 1 @@ -354,8 +271,10 @@ class TestGetKernelAvailability: ), ), ) - def test_get_kernel_availability(self, subprocess_output, expected_installed, expected_available, monkeypatch): - monkeypatch.setattr(system_info, "version", Version(7, 0)) + @centos7 + def test_get_kernel_availability( + self, pretend_os, subprocess_output, expected_installed, expected_available, monkeypatch + ): monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked(return_string=subprocess_output)) installed, available = pkghandler.get_kernel_availability() @@ -365,17 +284,16 @@ def test_get_kernel_availability(self, subprocess_output, expected_installed, ex class TestHandleNoNewerRHELKernelAvailable: - def test_handle_older_rhel_kernel_available(self, monkeypatch): - monkeypatch.setattr(system_info, "version", Version(7, 0)) - monkeypatch.setattr(system_info, "releasever", None) + @centos7 + def test_handle_older_rhel_kernel_available(self, pretend_os, monkeypatch): monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked(return_string=YUM_KERNEL_LIST_OLDER_AVAILABLE)) pkghandler.handle_no_newer_rhel_kernel_available() - assert utils.run_subprocess.cmd == ["yum", "install", "-y", "kernel-4.7.2-201.fc24"] + assert utils.run_subprocess.cmd == ["yum", "install", "-y", "--releasever=7Server", "kernel-4.7.2-201.fc24"] - def test_handle_older_rhel_kernel_not_available(self, monkeypatch): - monkeypatch.setattr(system_info, "version", Version(7, 0)) + @centos7 + def test_handle_older_rhel_kernel_not_available(self, pretend_os, monkeypatch): monkeypatch.setattr( utils, "run_subprocess", RunSubprocessMocked(return_string=YUM_KERNEL_LIST_OLDER_NOT_AVAILABLE) ) @@ -385,22 +303,21 @@ def test_handle_older_rhel_kernel_not_available(self, monkeypatch): assert pkghandler.replace_non_rhel_installed_kernel.call_count == 1 - def test_handle_older_rhel_kernel_not_available_multiple_installed(self, monkeypatch): - monkeypatch.setattr(system_info, "version", Version(7, 0)) - monkeypatch.setattr(system_info, "releasever", None) - monkeypatch.setattr(backup, "run_subprocess", RunSubprocessMocked()) + @centos7 + def test_handle_older_rhel_kernel_not_available_multiple_installed(self, pretend_os, monkeypatch): + monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked()) monkeypatch.setattr( utils, "run_subprocess", RunSubprocessMocked(return_string=YUM_KERNEL_LIST_OLDER_NOT_AVAILABLE_MULTIPLE_INSTALLED), ) - monkeypatch.setattr(pkghandler, "remove_pkgs", RemovePkgsMocked()) + monkeypatch.setattr(utils, "remove_pkgs", RemovePkgsMocked()) pkghandler.handle_no_newer_rhel_kernel_available() - assert len(pkghandler.remove_pkgs.pkgs) == 1 - assert pkghandler.remove_pkgs.pkgs[0] == "kernel-4.7.4-200.fc24" - assert utils.run_subprocess.cmd == ["yum", "install", "-y", "kernel-4.7.4-200.fc24"] + assert len(utils.remove_pkgs.pkgs) == 1 + assert utils.remove_pkgs.pkgs[0] == "kernel-4.7.4-200.fc24" + assert utils.run_subprocess.cmd == ["yum", "install", "-y", "--releasever=7Server", "kernel-4.7.4-200.fc24"] class TestReplaceNonRHELInstalledKernel: @@ -1239,8 +1156,8 @@ def test_get_system_packages_for_replacement(pretend_os, pkgs, expected, monkeyp monkeypatch.setattr( pkghandler, "get_installed_pkg_information", GetInstalledPkgInformationMocked(return_value=pkgs) ) - result = pkghandler.get_system_packages_for_replacement() + assert expected == result @@ -1554,24 +1471,6 @@ def test_get_packages_to_remove(monkeypatch): assert result[0].nevra.name == "installed_pkg" -def test_remove_pkgs_with_confirm(monkeypatch, tmpdir): - monkeypatch.setattr(utils, "ask_to_continue", mock.Mock()) - monkeypatch.setattr(pkghandler, "format_pkg_info", FormatPkgInfoMocked()) - monkeypatch.setattr(pkghandler, "remove_pkgs", RemovePkgsMocked()) - - pkghandler.remove_pkgs_unless_from_redhat( - [ - create_pkg_information( - packager="Oracle", vendor=None, name="installed_pkg", version="0.1", release="1", arch="x86_64" - ) - ], - str(tmpdir), - ) - - assert len(pkghandler.remove_pkgs.pkgs) == 1 - assert pkghandler.remove_pkgs.pkgs[0] == "installed_pkg-0.1-1.x86_64" - - @pytest.mark.parametrize( ("signature", "expected"), ( @@ -1744,7 +1643,7 @@ def test_remove_non_rhel_kernels(monkeypatch): GetInstalledPkgsWDifferentFingerprintMocked(pkg_selection="kernels"), ) monkeypatch.setattr(pkghandler, "format_pkg_info", FormatPkgInfoMocked()) - monkeypatch.setattr(pkghandler, "remove_pkgs", RemovePkgsMocked()) + monkeypatch.setattr(utils, "remove_pkgs", RemovePkgsMocked()) removed_pkgs = pkghandler.remove_non_rhel_kernels() @@ -1766,12 +1665,12 @@ def test_install_additional_rhel_kernel_pkgs(monkeypatch): GetInstalledPkgsWDifferentFingerprintMocked(pkg_selection="kernels"), ) monkeypatch.setattr(pkghandler, "format_pkg_info", FormatPkgInfoMocked()) - monkeypatch.setattr(pkghandler, "remove_pkgs", RemovePkgsMocked()) - monkeypatch.setattr(pkghandler, "call_yum_cmd", CallYumCmdMocked()) + monkeypatch.setattr(utils, "remove_pkgs", RemovePkgsMocked()) + monkeypatch.setattr(pkgmanager, "call_yum_cmd", CallYumCmdMocked()) removed_pkgs = pkghandler.remove_non_rhel_kernels() pkghandler.install_additional_rhel_kernel_pkgs(removed_pkgs) - assert pkghandler.call_yum_cmd.call_count == 2 + assert pkgmanager.call_yum_cmd.call_count == 2 @pytest.mark.parametrize( diff --git a/convert2rhel/unit_tests/pkgmanager/handlers/dnf/dnf_test.py b/convert2rhel/unit_tests/pkgmanager/handlers/dnf/dnf_test.py index 37ccb33af5..ea2fe69246 100644 --- a/convert2rhel/unit_tests/pkgmanager/handlers/dnf/dnf_test.py +++ b/convert2rhel/unit_tests/pkgmanager/handlers/dnf/dnf_test.py @@ -333,7 +333,7 @@ def test_process_transaction(self, pretend_os, validate_transaction, expected, c instance._set_up_base() instance._process_transaction(validate_transaction) - assert pkgmanager.Base.do_transaction.called_once() + assert pkgmanager.Base.do_transaction.call_count == 1 assert expected in caplog.records[-1].message @centos8 @@ -349,7 +349,7 @@ def test_process_transaction_exceptions(self, pretend_os, caplog): with pytest.raises(exceptions.CriticalError): instance._process_transaction(validate_transaction=False) - assert pkgmanager.Base.do_transaction.called_once() + assert pkgmanager.Base.do_transaction.call_count == 1 assert "Failed to validate the dnf transaction." in caplog.records[-1].message @centos8 diff --git a/convert2rhel/unit_tests/pkgmanager/handlers/yum/yum_test.py b/convert2rhel/unit_tests/pkgmanager/handlers/yum/yum_test.py index e16ee39bd5..5822dc83a3 100644 --- a/convert2rhel/unit_tests/pkgmanager/handlers/yum/yum_test.py +++ b/convert2rhel/unit_tests/pkgmanager/handlers/yum/yum_test.py @@ -22,7 +22,11 @@ import pytest import six -from convert2rhel import exceptions, pkghandler, pkgmanager, unit_tests, utils + +six.add_move(six.MovedModule("mock", "mock", "unittest.mock")) +from six.moves import mock + +from convert2rhel import backup, exceptions, pkghandler, pkgmanager from convert2rhel.pkgmanager.handlers.yum import YumTransactionHandler from convert2rhel.repo import DEFAULT_YUM_REPOFILE_DIR, DEFAULT_YUM_VARS_DIR from convert2rhel.systeminfo import system_info @@ -30,29 +34,6 @@ from convert2rhel.unit_tests.conftest import centos7 -six.add_move(six.MovedModule("mock", "mock", "unittest.mock")) -from six.moves import mock - - -class YumResolveDepsMocked(unit_tests.MockFunctionObject): - spec = pkgmanager.handlers.yum.YumTransactionHandler._resolve_dependencies - - def __init__(self, start_at=0, loop_until=2, **kwargs): - super(YumResolveDepsMocked, self).__init__(**kwargs) - - self.loop_until = loop_until - # Note: This means call_count and len(call_args_list) won't match. - self._mock.call_count = start_at - - def __call__(self, *args, **kwargs): - super(YumResolveDepsMocked, self).__call__(*args, **kwargs) - - if self._mock.call_count >= self.loop_until: - return True - else: - return False - - SYSTEM_PACKAGES = [ create_pkg_information( packager="test", @@ -107,6 +88,7 @@ def _mock_yum_api_calls(self, monkeypatch): monkeypatch.setattr(pkgmanager.YumBase, "processTransaction", value=mock.Mock()) monkeypatch.setattr(pkgmanager.YumBase, "install", value=mock.Mock()) monkeypatch.setattr(pkgmanager.YumBase, "remove", value=mock.Mock()) + monkeypatch.setattr(pkgmanager.YumBase, "close", value=mock.Mock()) @centos7 def test_set_up_base(self, pretend_os): @@ -117,7 +99,16 @@ def test_set_up_base(self, pretend_os): assert instance._base.conf.yumvar["releasever"] == "7Server" @centos7 - @pytest.mark.parametrize(("enabled_rhel_repos"), ((["rhel-7-test-repo"]))) + @pytest.mark.parametrize( + ("enabled_rhel_repos",), + ( + ( + [ + "rhel-7-test-repo", + ], + ), + ), + ) def test_enable_repos(self, pretend_os, enabled_rhel_repos, caplog, monkeypatch): instance = YumTransactionHandler() instance._set_up_base() @@ -125,12 +116,21 @@ def test_enable_repos(self, pretend_os, enabled_rhel_repos, caplog, monkeypatch) monkeypatch.setattr(system_info, "get_enabled_rhel_repos", lambda: enabled_rhel_repos) instance._enable_repos() - assert "Enabling RHEL repositories:\n%s" % "\n".join(enabled_rhel_repos) in caplog.records[-1].message + assert "Enabling RHEL repositories:\n%s" % "".join(enabled_rhel_repos) in caplog.records[-1].message assert pkgmanager.RepoStorage.disableRepo.called_once() assert pkgmanager.RepoStorage.enableRepo.call_count == len(enabled_rhel_repos) @centos7 - @pytest.mark.parametrize(("enabled_rhel_repos"), ((["rhel-7-test-repo"]))) + @pytest.mark.parametrize( + ("enabled_rhel_repos",), + ( + ( + [ + "rhel-7-test-repo", + ], + ), + ), + ) def test_enable_repos_repo_error(self, pretend_os, enabled_rhel_repos, caplog, monkeypatch): instance = YumTransactionHandler() instance._set_up_base() @@ -140,7 +140,8 @@ def test_enable_repos_repo_error(self, pretend_os, enabled_rhel_repos, caplog, m with pytest.raises(exceptions.CriticalError): instance._enable_repos() - assert pkgmanager.RepoStorage.disableRepo.called_once() + assert pkgmanager.RepoStorage.disableRepo.call_count == 1 + assert "Failed to populate repository metadata." in caplog.records[-1].message @centos7 def test_perform_operations(self, pretend_os, monkeypatch): @@ -149,7 +150,7 @@ def test_perform_operations(self, pretend_os, monkeypatch): monkeypatch.setattr(pkghandler, "get_installed_pkg_information", lambda: SYSTEM_PACKAGES) monkeypatch.setattr(YumTransactionHandler, "_swap_base_os_specific_packages", swap_base_os_specific_packages) instance = YumTransactionHandler() - + instance._set_up_base() instance._perform_operations() assert pkgmanager.YumBase.update.call_count == len(SYSTEM_PACKAGES) @@ -162,7 +163,7 @@ def test_perform_operations_reinstall_exception(self, pretend_os, caplog, monkey monkeypatch.setattr(pkghandler, "get_installed_pkg_information", lambda: SYSTEM_PACKAGES) pkgmanager.YumBase.reinstall.side_effect = pkgmanager.Errors.ReinstallInstallError instance = YumTransactionHandler() - + instance._set_up_base() instance._perform_operations() assert pkgmanager.YumBase.reinstall.call_count == len(SYSTEM_PACKAGES) @@ -175,7 +176,7 @@ def test_perform_operations_downgrade_exception(self, pretend_os, caplog, monkey pkgmanager.YumBase.reinstall.side_effect = pkgmanager.Errors.ReinstallInstallError pkgmanager.YumBase.downgrade.side_effect = pkgmanager.Errors.ReinstallRemoveError instance = YumTransactionHandler() - + instance._set_up_base() instance._perform_operations() assert pkgmanager.YumBase.reinstall.call_count == len(SYSTEM_PACKAGES) @@ -187,23 +188,23 @@ def test_perform_operations_no_more_mirrors_repo_exception(self, pretend_os, mon monkeypatch.setattr(pkghandler, "get_installed_pkg_information", lambda: SYSTEM_PACKAGES) pkgmanager.YumBase.update.side_effect = pkgmanager.Errors.NoMoreMirrorsRepoError instance = YumTransactionHandler() - + instance._set_up_base() with pytest.raises(exceptions.CriticalError): instance._perform_operations() @centos7 @pytest.mark.parametrize( - ("ret_code", "message", "validate_transaction", "expected"), ( - (0, "success", True, True), - (1, "failed", True, False), - (1, "failed", False, False), - (1, "Depsolving loop limit reached", True, False), + "ret_code", + "message", + "expected", + ), + ( + (0, "success", None), + (1, "failed", "failed"), ), ) - def test_resolve_dependencies( - self, pretend_os, ret_code, message, validate_transaction, expected, caplog, monkeypatch - ): + def test_resolve_dependencies(self, pretend_os, ret_code, message, expected, monkeypatch): monkeypatch.setattr( pkgmanager.YumBase, "resolveDeps", @@ -212,13 +213,10 @@ def test_resolve_dependencies( message, ), ) - monkeypatch.setattr(pkgmanager.handlers.yum, "_resolve_yum_problematic_dependencies", mock.Mock()) instance = YumTransactionHandler() instance._set_up_base() - result = instance._resolve_dependencies(validate_transaction) + result = instance._resolve_dependencies() - if expected: - assert pkgmanager.handlers.yum._resolve_yum_problematic_dependencies.call_count == 0 assert result == expected @pytest.mark.parametrize( @@ -248,66 +246,6 @@ def test_process_transaction_with_exceptions(self, pretend_os, caplog): assert "Failed to validate the yum transaction." in caplog.records[-1].message - @centos7 - @pytest.mark.parametrize( - ("validate_transaction"), - ( - (True,), - (False,), - ), - ) - def test_run_transaction(self, pretend_os, validate_transaction, caplog, monkeypatch): - monkeypatch.setattr(pkgmanager.handlers.yum.YumTransactionHandler, "_perform_operations", mock.Mock()) - monkeypatch.setattr( - pkgmanager.handlers.yum.YumTransactionHandler, "_resolve_dependencies", YumResolveDepsMocked(loop_until=0) - ) - monkeypatch.setattr(pkgmanager.handlers.yum.YumTransactionHandler, "_process_transaction", mock.Mock()) - # Save original function as we need to override the decorator that is in place for `run_transaction` - original_func = pkgmanager.handlers.yum.YumTransactionHandler.run_transaction.__wrapped__ - monkeypatch.setattr( - pkgmanager.handlers.yum.YumTransactionHandler, "run_transaction", mock_decorator(original_func) - ) - instance = YumTransactionHandler() - instance._set_up_base() - instance.run_transaction(validate_transaction=validate_transaction) - - assert pkgmanager.handlers.yum.YumTransactionHandler._perform_operations.call_count == 1 - assert pkgmanager.handlers.yum.YumTransactionHandler._process_transaction.call_count == 1 - - @centos7 - @pytest.mark.parametrize( - ("start_at", "loop_until", "expected_count"), - ( - (0, 99, (4, 4)), - (4, 99, (4, 8)), - ), - ) - def test_run_transaction_resolve_dependencies_loop( - self, pretend_os, start_at, loop_until, expected_count, monkeypatch - ): - monkeypatch.setattr(pkgmanager.handlers.yum.YumTransactionHandler, "_perform_operations", mock.Mock()) - monkeypatch.setattr( - pkgmanager.handlers.yum.YumTransactionHandler, - "_resolve_dependencies", - YumResolveDepsMocked(start_at, loop_until), - ) - # Save original function as we need to override the decorator that is in place for `run_transaction` - original_func = pkgmanager.handlers.yum.YumTransactionHandler.run_transaction.__wrapped__ - monkeypatch.setattr( - pkgmanager.handlers.yum.YumTransactionHandler, "run_transaction", mock_decorator(original_func) - ) - instance = YumTransactionHandler() - instance._set_up_base() - - with pytest.raises(exceptions.CriticalError): - instance.run_transaction(validate_transaction=False) - - perform_operations_count, resolve_dependencies_count = expected_count - assert pkgmanager.handlers.yum.YumTransactionHandler._perform_operations.call_count == perform_operations_count - assert ( - pkgmanager.handlers.yum.YumTransactionHandler._resolve_dependencies.call_count == resolve_dependencies_count - ) - @centos7 def test_package_marked_for_update(self, pretend_os, monkeypatch): """ @@ -322,6 +260,7 @@ def test_package_marked_for_update(self, pretend_os, monkeypatch): ] # We don't care about the value, only that if has something. instance = YumTransactionHandler() + instance._set_up_base() instance._perform_operations() assert pkgmanager.YumBase.update.call_count == len(SYSTEM_PACKAGES) @@ -360,6 +299,70 @@ def return_installed(pkg): assert pkgmanager.YumBase.remove.call_count == swaps assert pkgmanager.YumBase.install.call_count == swaps + @centos7 + @pytest.mark.parametrize( + ( + "messages", + "expected", + ), + ( + ("Test message", "Test message"), + (None, None), + ), + ) + def test_run_transaction_subprocess(self, pretend_os, monkeypatch, messages, expected): + monkeypatch.setattr(YumTransactionHandler, "_perform_operations", mock.Mock()) + monkeypatch.setattr(YumTransactionHandler, "_resolve_dependencies", mock.Mock(return_value=messages)) + monkeypatch.setattr(YumTransactionHandler, "_process_transaction", mock.Mock()) + + original_func = YumTransactionHandler._run_transaction_subprocess.__wrapped__ + monkeypatch.setattr(YumTransactionHandler, "_run_transaction_subprocess", mock_decorator(original_func)) + + instance = YumTransactionHandler() + result = instance._run_transaction_subprocess(validate_transaction=True) + + assert instance._perform_operations.call_count == 1 + assert instance._resolve_dependencies.call_count == 1 + + if not messages: + assert instance._process_transaction.call_count == 1 + + assert result == expected + + @centos7 + def test_run_transaction(self, pretend_os, monkeypatch, caplog): + monkeypatch.setattr(YumTransactionHandler, "_run_transaction_subprocess", mock.Mock(return_value=None)) + instance = YumTransactionHandler() + instance.run_transaction(True) + + # No messages in the output, meaning that it worked. + assert len(caplog.records) == 0 + + @centos7 + def test_run_transaction_reached_loop_max_attempts(self, pretend_os, monkeypatch, caplog): + monkeypatch.setattr(pkgmanager.handlers.yum, "MAX_NUM_OF_ATTEMPTS_TO_RESOLVE_DEPS", 1) + monkeypatch.setattr( + YumTransactionHandler, + "_run_transaction_subprocess", + mock.Mock(return_value="Depsolving loop limit reached"), + ) + instance = YumTransactionHandler() + with pytest.raises(exceptions.CriticalError): + instance.run_transaction(True) + + assert "Retrying to resolve dependencies 1" in caplog.records[-2].message + assert "Failed to resolve dependencies in the transaction." in caplog.records[-1].message + + @centos7 + def test_run_transaction_critical_error_exception(self, _mock_yum_api_calls, pretend_os, monkeypatch, caplog): + monkeypatch.setattr(pkgmanager.handlers.yum, "MAX_NUM_OF_ATTEMPTS_TO_RESOLVE_DEPS", -1) + instance = YumTransactionHandler() + instance._set_up_base() + with pytest.raises(exceptions.CriticalError): + instance.run_transaction(True) + + assert "Failed to resolve dependencies in the transaction." in caplog.records[-1].message + @centos7 @pytest.mark.parametrize( @@ -419,21 +422,23 @@ def test_resolve_yum_problematic_dependencies( monkeypatch, caplog, ): + monkeypatch.setattr(pkgmanager.handlers.yum.backup, "backup_control", mock.Mock()) + monkeypatch.setattr(pkgmanager.handlers.yum, "RestorablePackage", mock.Mock()) monkeypatch.setattr(pkgmanager.handlers.yum, "remove_pkgs", RemovePkgsMocked()) pkgmanager.handlers.yum._resolve_yum_problematic_dependencies(output) if expected_remove_pkgs: assert pkgmanager.handlers.yum.remove_pkgs.called - backedup_reposdir = os.path.join(utils.BACKUP_DIR, hashlib.md5(DEFAULT_YUM_REPOFILE_DIR.encode()).hexdigest()) - backedup_yum_varsdir = os.path.join(utils.BACKUP_DIR, hashlib.md5(DEFAULT_YUM_VARS_DIR.encode()).hexdigest()) - pkgmanager.handlers.yum.remove_pkgs.assert_called_with( - pkgs_to_remove=expected_remove_pkgs, - backup=True, - critical=True, + backedup_reposdir = os.path.join(backup.BACKUP_DIR, hashlib.md5(DEFAULT_YUM_REPOFILE_DIR.encode()).hexdigest()) + backedup_yum_varsdir = os.path.join(backup.BACKUP_DIR, hashlib.md5(DEFAULT_YUM_VARS_DIR.encode()).hexdigest()) + assert pkgmanager.handlers.yum.RestorablePackage.called + pkgmanager.handlers.yum.RestorablePackage.assert_called_with( + pkgs=expected_remove_pkgs, reposdir=backedup_reposdir, set_releasever=True, custom_releasever=7, varsdir=backedup_yum_varsdir, ) + pkgmanager.handlers.yum.remove_pkgs.assert_called_with(pkgs_to_remove=expected_remove_pkgs, critical=True) else: assert "Unable to resolve dependency issues." in caplog.records[-1].message diff --git a/convert2rhel/unit_tests/pkgmanager/pkgmanager_test.py b/convert2rhel/unit_tests/pkgmanager/pkgmanager_test.py index 4f7890b9e4..cb6e2fbc51 100644 --- a/convert2rhel/unit_tests/pkgmanager/pkgmanager_test.py +++ b/convert2rhel/unit_tests/pkgmanager/pkgmanager_test.py @@ -20,8 +20,11 @@ import pytest import six -from convert2rhel import pkgmanager -from convert2rhel.unit_tests import run_subprocess_side_effect +from convert2rhel import pkgmanager, utils +from convert2rhel.systeminfo import Version, system_info +from convert2rhel.toolopts import tool_opts +from convert2rhel.unit_tests import RunSubprocessMocked, run_subprocess_side_effect +from convert2rhel.unit_tests.conftest import centos7, centos8 six.add_move(six.MovedModule("mock", "mock", "unittest.mock")) @@ -83,3 +86,186 @@ def test_rpm_db_lock(): pass assert pkg_obj_mock.rpmdb is None + + +class TestCallYumCmd: + def test_call_yum_cmd(self, monkeypatch): + monkeypatch.setattr(system_info, "version", Version(8, 0)) + monkeypatch.setattr(system_info, "releasever", "8") + monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked()) + + pkgmanager.call_yum_cmd("install") + + assert utils.run_subprocess.cmd == [ + "yum", + "install", + "-y", + "--releasever=8", + "--setopt=module_platform_id=platform:el8", + ] + + @centos7 + def test_call_yum_cmd_not_setting_releasever(self, pretend_os, monkeypatch): + monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked()) + + pkgmanager.call_yum_cmd("install", set_releasever=False) + + assert utils.run_subprocess.cmd == ["yum", "install", "-y"] + + @centos7 + def test_call_yum_cmd_with_disablerepo_and_enablerepo(self, pretend_os, monkeypatch): + monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked()) + monkeypatch.setattr(tool_opts, "no_rhsm", True) + monkeypatch.setattr(tool_opts, "disablerepo", ["*"]) + monkeypatch.setattr(tool_opts, "enablerepo", ["rhel-7-extras-rpm"]) + + pkgmanager.call_yum_cmd("install") + + assert utils.run_subprocess.cmd == [ + "yum", + "install", + "-y", + "--disablerepo=*", + "--releasever=7Server", + "--enablerepo=rhel-7-extras-rpm", + ] + + @centos7 + def test_call_yum_cmd_with_submgr_enabled_repos(self, pretend_os, monkeypatch): + monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked()) + monkeypatch.setattr(system_info, "submgr_enabled_repos", ["rhel-7-extras-rpm"]) + monkeypatch.setattr(tool_opts, "enablerepo", ["not-to-be-used-in-the-yum-call"]) + + pkgmanager.call_yum_cmd("install") + + assert utils.run_subprocess.cmd == [ + "yum", + "install", + "-y", + "--releasever=7Server", + "--enablerepo=rhel-7-extras-rpm", + ] + + @centos7 + def test_call_yum_cmd_with_repo_overrides(self, pretend_os, monkeypatch): + monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked()) + monkeypatch.setattr(system_info, "submgr_enabled_repos", ["not-to-be-used-in-the-yum-call"]) + monkeypatch.setattr(tool_opts, "enablerepo", ["not-to-be-used-in-the-yum-call"]) + + pkgmanager.call_yum_cmd("install", ["pkg"], enable_repos=[], disable_repos=[]) + + assert utils.run_subprocess.cmd == ["yum", "install", "-y", "--releasever=7Server", "pkg"] + + pkgmanager.call_yum_cmd( + "install", + ["pkg"], + enable_repos=["enable-repo"], + disable_repos=["disable-repo"], + ) + + assert utils.run_subprocess.cmd == [ + "yum", + "install", + "-y", + "--disablerepo=disable-repo", + "--releasever=7Server", + "--enablerepo=enable-repo", + "pkg", + ] + + @centos8 + def test_call_yum_cmd_nothing_to_do(self, pretend_os, monkeypatch, caplog): + monkeypatch.setattr( + utils, "run_subprocess", RunSubprocessMocked(return_code=1, return_string="Error: Nothing to do\n") + ) + stdout, returncode = pkgmanager.call_yum_cmd("install", ["pkg"], enable_repos=[], disable_repos=[]) + + assert returncode == 0 + assert stdout == "Error: Nothing to do\n" + assert utils.run_subprocess.cmd == [ + "yum", + "install", + "-y", + "--releasever=8.5", + "--setopt=module_platform_id=platform:el8", + "pkg", + ] + assert "Yum has nothing to do. Ignoring" in caplog.records[-1].message + + @centos8 + def test_call_yum_cmd_reposdir_set(self, pretend_os, monkeypatch, caplog, tmpdir): + reposdir = str(tmpdir) + monkeypatch.setattr( + utils, "run_subprocess", RunSubprocessMocked(return_code=1, return_string="Error: Nothing to do\n") + ) + stdout, returncode = pkgmanager.call_yum_cmd( + "install", ["pkg"], enable_repos=[], disable_repos=[], reposdir=reposdir + ) + + assert returncode == 0 + assert stdout == "Error: Nothing to do\n" + assert utils.run_subprocess.cmd == [ + "yum", + "install", + "-y", + "--releasever=8.5", + "--setopt=module_platform_id=platform:el8", + "--setopt=reposdir=%s" % reposdir, + "pkg", + ] + assert "Yum has nothing to do. Ignoring" in caplog.records[-1].message + + @centos8 + def test_call_yum_cmd_varsdir_set(self, pretend_os, monkeypatch, caplog, tmpdir): + varsdir = str(tmpdir) + monkeypatch.setattr( + utils, "run_subprocess", RunSubprocessMocked(return_code=1, return_string="Error: Nothing to do\n") + ) + stdout, returncode = pkgmanager.call_yum_cmd( + "install", ["pkg"], enable_repos=[], disable_repos=[], varsdir=varsdir + ) + + assert returncode == 0 + assert stdout == "Error: Nothing to do\n" + assert utils.run_subprocess.cmd == [ + "yum", + "install", + "-y", + "--releasever=8.5", + "--setopt=varsdir=%s" % varsdir, + "--setopt=module_platform_id=platform:el8", + "pkg", + ] + assert "Yum has nothing to do. Ignoring" in caplog.records[-1].message + + @centos8 + def test_call_yum_cmd_custom_release_set(self, pretend_os, monkeypatch, caplog): + monkeypatch.setattr( + utils, "run_subprocess", RunSubprocessMocked(return_code=1, return_string="Error: Nothing to do\n") + ) + stdout, returncode = pkgmanager.call_yum_cmd( + "install", ["pkg"], enable_repos=[], disable_repos=[], custom_releasever="8" + ) + + assert returncode == 0 + assert stdout == "Error: Nothing to do\n" + assert utils.run_subprocess.cmd == [ + "yum", + "install", + "-y", + "--releasever=8", + "--setopt=module_platform_id=platform:el8", + "pkg", + ] + assert "Yum has nothing to do. Ignoring" in caplog.records[-1].message + + @centos8 + def test_call_yum_cmd_assertion_error(self, pretend_os, monkeypatch, global_system_info): + monkeypatch.setattr(pkgmanager, "system_info", global_system_info) + global_system_info.releasever = None + monkeypatch.setattr( + utils, "run_subprocess", RunSubprocessMocked(return_code=1, return_string="Error: Nothing to do\n") + ) + + with pytest.raises(AssertionError, match="custom_releasever or system_info.releasever must be set."): + pkgmanager.call_yum_cmd("install", ["pkg"], enable_repos=[], disable_repos=[], custom_releasever=None) diff --git a/convert2rhel/unit_tests/subscription_test.py b/convert2rhel/unit_tests/subscription_test.py index 3c9cb8f383..9e91d1448b 100644 --- a/convert2rhel/unit_tests/subscription_test.py +++ b/convert2rhel/unit_tests/subscription_test.py @@ -360,11 +360,11 @@ def test_restore_subman_uninstalled(self, caplog, monkeypatch, system_subscripti def test_install_rhel_subsription_manager(monkeypatch, global_backup_control): mock_backup_control = mock.Mock() - monkeypatch.setattr(global_backup_control, "push", mock_backup_control) + monkeypatch.setattr(subscription.backup.backup_control, "push", mock_backup_control) subscription.install_rhel_subscription_manager(["subscription-manager", "json-c.x86_64"]) - assert mock_backup_control.push.called_once + assert mock_backup_control.call_count == 1 @pytest.mark.usefixtures("tool_opts", scope="function") @@ -675,7 +675,7 @@ def test_from_tool_opts_interactive_data(self, registration_kwargs, prompt_input def prompt_user(prompt, password=False): if prompt in prompt_input: return prompt_input[prompt] - raise Exception("Should not have been called with that prompt for the input") + raise TypeError("Should not have been called with that prompt for the input") monkeypatch.setattr(utils, "prompt_user", PromptUserMocked(side_effect=prompt_user)) @@ -992,9 +992,6 @@ def test_verify_rhsm_installed_failure(self, monkeypatch, caplog): assert "The subscription-manager package is not installed correctly." in caplog.text -# ---- - - def test_get_pool_id(): SUBSCRIPTION_DETAILS = ( "Subscription Name: Good subscription\n" @@ -1045,8 +1042,17 @@ def test_get_pool_id(): ) @centos8 def test_enable_repos_rhel_repoids( - pretend_os, rhel_repoids, subprocess, should_raise, expected, expected_message, monkeypatch, caplog + pretend_os, + rhel_repoids, + subprocess, + should_raise, + expected, + expected_message, + monkeypatch, + caplog, + global_system_info, ): + monkeypatch.setattr(subscription, "system_info", global_system_info) cmd_mock = ["subscription-manager", "repos"] for repo in rhel_repoids: cmd_mock.append("--enable=%s" % repo) @@ -1067,7 +1073,7 @@ def test_enable_repos_rhel_repoids( subscription.enable_repos(rhel_repoids=rhel_repoids) else: subscription.enable_repos(rhel_repoids=rhel_repoids) - assert system_info.submgr_enabled_repos == expected + assert subscription.system_info.submgr_enabled_repos == expected assert expected_message in caplog.records[-1].message assert run_subprocess_mock.call_count == 1 @@ -1105,7 +1111,9 @@ def test_enable_repos_rhel_repoids_fallback_default_rhsm( expected, monkeypatch, caplog, + global_system_info, ): + monkeypatch.setattr(subscription, "system_info", global_system_info) cmd_mock = ["subscription-manager", "repos"] for repo in rhel_repoids: cmd_mock.append("--enable=%s" % repo) @@ -1116,14 +1124,15 @@ def test_enable_repos_rhel_repoids_fallback_default_rhsm( "run_subprocess", value=run_subprocess_mock, ) - monkeypatch.setattr(system_info, "default_rhsm_repoids", value=default_rhsm_repoids) + monkeypatch.setattr(subscription.system_info, "default_rhsm_repoids", default_rhsm_repoids) + monkeypatch.setattr(subscription.system_info, "eus_rhsm_repoids", rhel_repoids) if should_raise: with pytest.raises(SystemExit): subscription.enable_repos(rhel_repoids=rhel_repoids) else: subscription.enable_repos(rhel_repoids=rhel_repoids) - assert system_info.submgr_enabled_repos == default_rhsm_repoids + assert subscription.system_info.submgr_enabled_repos == default_rhsm_repoids assert expected in caplog.records[-1].message assert run_subprocess_mock.call_count == 2 @@ -1164,7 +1173,9 @@ def test_enable_repos_toolopts_enablerepo( tool_opts, monkeypatch, caplog, + global_system_info, ): + monkeypatch.setattr(subscription, "system_info", global_system_info) cmd_mock = ["subscription-manager", "repos"] for repo in toolopts_enablerepo: cmd_mock.append("--enable=%s" % repo) @@ -1180,14 +1191,13 @@ def test_enable_repos_toolopts_enablerepo( value=run_subprocess_mock, ) tool_opts.enablerepo = toolopts_enablerepo - # monkeypatch.setattr(tool_opts, "enablerepo", toolopts_enablerepo) if should_raise: with pytest.raises(SystemExit): subscription.enable_repos(rhel_repoids=None) else: subscription.enable_repos(rhel_repoids=None) - assert system_info.submgr_enabled_repos == expected + assert subscription.system_info.submgr_enabled_repos == expected assert expected_message in caplog.records[-1].message assert run_subprocess_mock.call_count == 1 diff --git a/convert2rhel/unit_tests/toolopts_test.py b/convert2rhel/unit_tests/toolopts_test.py index 9d95ba7592..80bc35be03 100644 --- a/convert2rhel/unit_tests/toolopts_test.py +++ b/convert2rhel/unit_tests/toolopts_test.py @@ -63,9 +63,7 @@ def test_cmdline_disablerepo_defaults_to_asterisk(self, monkeypatch, global_tool assert global_tool_opts.enablerepo == ["foo"] assert global_tool_opts.disablerepo == ["*"] - # # Parsing of serverurl - # @pytest.mark.parametrize( ("serverurl", "hostname", "port", "prefix"), @@ -141,7 +139,7 @@ def test_serverurl_with_no_rhsm_credentials(self, caplog, monkeypatch, global_to assert message in caplog.text -def test_keep_rhsm(monkeypatch, caplog): +def test_keep_rhsm(monkeypatch, caplog, global_tool_opts): monkeypatch.setattr(sys, "argv", mock_cli_arguments(["--keep-rhsm"])) convert2rhel.toolopts.CLI() @@ -190,7 +188,9 @@ def test_cmdline_obsolete_variant_option(argv, warn, ask_to_continue, monkeypatc ) @mock.patch("convert2rhel.toolopts.tool_opts.no_rhsm", False) @mock.patch("convert2rhel.toolopts.tool_opts.enablerepo", []) -def test_both_disable_submgr_and_no_rhsm_options_work(argv, raise_exception, no_rhsm_value, monkeypatch, caplog): +def test_both_disable_submgr_and_no_rhsm_options_work( + argv, raise_exception, no_rhsm_value, monkeypatch, caplog, global_tool_opts +): monkeypatch.setattr(sys, "argv", argv) if raise_exception: @@ -348,7 +348,7 @@ def test_multiple_auth_src_combined(argv, content, message, output, caplog, monk ), ), ) -def test_multiple_auth_src_files(argv, content, message, output, caplog, monkeypatch, tmpdir): +def test_multiple_auth_src_files(argv, content, message, output, caplog, monkeypatch, tmpdir, global_tool_opts): """Test combination of password file, config file and CLI.""" path0 = os.path.join(str(tmpdir), "convert2rhel.password") with open(path0, "w") as file: @@ -381,7 +381,7 @@ def test_multiple_auth_src_files(argv, content, message, output, caplog, monkeyp ), ), ) -def test_multiple_auth_src_cli(argv, message, output, caplog, monkeypatch): +def test_multiple_auth_src_cli(argv, message, output, caplog, monkeypatch, global_tool_opts): """Test both auth methods in CLI.""" monkeypatch.setattr(sys, "argv", argv) monkeypatch.setattr(convert2rhel.toolopts, "CONFIG_PATHS", value=[""]) diff --git a/convert2rhel/unit_tests/utils_test.py b/convert2rhel/unit_tests/utils_test.py index efa241db66..bdb92ab3ad 100644 --- a/convert2rhel/unit_tests/utils_test.py +++ b/convert2rhel/unit_tests/utils_test.py @@ -38,7 +38,7 @@ from six.moves import mock -from convert2rhel import systeminfo, toolopts, unit_tests, utils # Imports unit_tests/__init__.py +from convert2rhel import exceptions, systeminfo, toolopts, unit_tests, utils # Imports unit_tests/__init__.py from convert2rhel.systeminfo import system_info from convert2rhel.unit_tests import RunCmdInPtyMocked, RunSubprocessMocked, is_rpm_based_os @@ -1035,3 +1035,72 @@ def test_run_as_child_process_with_keyboard_interrupt(monkeypatch): decorated = utils.run_as_child_process(RunAsChildProcessFunctions.raise_keyboard_interrupt_exception) with pytest.raises(KeyboardInterrupt): decorated((), {}) + + +class TestRemovePkgs: + def test_remove_pkgs_without_backup(self, monkeypatch): + monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked()) + pkgs = ["pkg1", "pkg2", "pkg3"] + + utils.remove_pkgs(pkgs, False) + + assert utils.run_subprocess.call_count == len(pkgs) + + rpm_remove_cmd = ["rpm", "-e", "--nodeps"] + for cmd, pkg in zip(utils.run_subprocess.cmds, pkgs): + assert rpm_remove_cmd + [pkg] == cmd + + @pytest.mark.parametrize( + ("pkgs_to_remove", "ret_code", "critical", "expected"), + ( + (["pkg1"], 1, True, "Error: Couldn't remove {0}."), + (["pkg1"], 1, False, "Couldn't remove {0}."), + ), + ) + def test_remove_pkgs_failed_to_remove( + self, + pkgs_to_remove, + ret_code, + critical, + expected, + monkeypatch, + caplog, + ): + run_subprocess_mock = RunSubprocessMocked( + side_effect=unit_tests.run_subprocess_side_effect( + (("rpm", "-e", "--nodeps", pkgs_to_remove[0]), ("test", ret_code)), + ) + ) + monkeypatch.setattr( + utils, + "run_subprocess", + value=run_subprocess_mock, + ) + + if critical: + with pytest.raises(exceptions.CriticalError): + utils.remove_pkgs( + pkgs_to_remove=pkgs_to_remove, + critical=critical, + ) + else: + utils.remove_pkgs(pkgs_to_remove=pkgs_to_remove, critical=critical) + + assert expected.format(pkgs_to_remove[0]) in caplog.records[-1].message + + def test_remove_pkgs_with_empty_list(self, caplog): + utils.remove_pkgs([]) + assert "No package to remove" in caplog.messages[-1] + + +@pytest.mark.parametrize( + ("pkg_nevra", "nvra_without_epoch"), + ( + ("7:oraclelinux-release-7.9-1.0.9.el7.x86_64", "oraclelinux-release-7.9-1.0.9.el7.x86_64"), + ("oraclelinux-release-8:8.2-1.0.8.el8.x86_64", "oraclelinux-release-8:8.2-1.0.8.el8.x86_64"), + ("1:mod_proxy_html-2.4.6-97.el7.centos.5.x86_64", "mod_proxy_html-2.4.6-97.el7.centos.5.x86_64"), + ("httpd-tools-2.4.6-97.el7.centos.5.x86_64", "httpd-tools-2.4.6-97.el7.centos.5.x86_64"), + ), +) +def test_remove_epoch_from_yum_nevra_notation(pkg_nevra, nvra_without_epoch): + assert utils._remove_epoch_from_yum_nevra_notation(pkg_nevra) == nvra_without_epoch diff --git a/convert2rhel/utils.py b/convert2rhel/utils.py index 27bd9bb585..a0ba090d82 100644 --- a/convert2rhel/utils.py +++ b/convert2rhel/utils.py @@ -40,7 +40,7 @@ from six import moves -from convert2rhel import i18n +from convert2rhel import exceptions, i18n loggerinst = logging.getLogger(__name__) @@ -70,7 +70,6 @@ class Color: DATA_DIR = "/usr/share/convert2rhel/" # Directory for temporary data to be stored during runtime TMP_DIR = "/var/lib/convert2rhel/" -BACKUP_DIR = os.path.join(TMP_DIR, "backup") class UnableToSerialize(Exception): @@ -702,6 +701,72 @@ def download_pkg( return path +def remove_pkgs(pkgs_to_remove, critical=True): + """Remove packages not heeding to their dependencies. + + .. note:: + Following the work on https://github.com/oamg/convert2rhel/pull/1041, + we might move this on it's own utils module. + + :param pkgs_to_remove list[str]: List of packages to remove. + :param critical bool: If it should raise an exception in case of failure of + removing a package. + :returns list[str]: A list of packages removed. If no packages are provided + to remove, an empty list will be returned. + """ + pkgs_removed = [] + + if not pkgs_to_remove: + loggerinst.info("No package to remove") + return pkgs_removed + + pkgs_failed_to_remove = [] + for nevra in pkgs_to_remove: + # It's necessary to remove an epoch from the NEVRA string returned by yum because the rpm command does not + # handle the epoch well and considers the package we want to remove as not installed. On the other hand, the + # epoch in NEVRA returned by dnf is handled by rpm just fine. + nvra = _remove_epoch_from_yum_nevra_notation(nevra) + loggerinst.info("Removing package: %s" % nvra) + _, ret_code = run_subprocess(["rpm", "-e", "--nodeps", nvra]) + if ret_code != 0: + pkgs_failed_to_remove.append(nevra) + else: + pkgs_removed.append(nevra) + + if pkgs_failed_to_remove: + pkgs_as_str = format_sequence_as_message(pkgs_failed_to_remove) + if critical: + loggerinst.critical_no_exit("Error: Couldn't remove %s." % pkgs_as_str) + raise exceptions.CriticalError( + id_="FAILED_TO_REMOVE_PACKAGES", + title="Couldn't remove packages.", + description="While attempting to roll back changes, we encountered an unexpected failure while attempting to remove one or more of the packages we installed earlier.", + diagnosis="Couldn't remove %s." % pkgs_as_str, + ) + else: + loggerinst.warning("Couldn't remove %s." % pkgs_as_str) + + return pkgs_removed + + +def _remove_epoch_from_yum_nevra_notation(package_nevra): + """Remove epoch from the NEVRA string returned by yum. + + Yum prints epoch only when it's non-zero. It's printed differently by yum and dnf: + yum - epoch before name: "7:oraclelinux-release-7.9-1.0.9.el7.x86_64" + dnf - epoch before version: "oraclelinux-release-8:8.2-1.0.8.el8.x86_64" + + This function removes the epoch from the yum notation only. + It's safe to pass the dnf notation string with an epoch. This function will return it as is. + """ + epoch_match = re.search(r"^\d+:(.*)", package_nevra) + if epoch_match: + # Return NVRA without the found epoch + return epoch_match.group(1) + + return package_nevra + + def report_on_a_download_error(output, pkg): """ Report on a failure to download a package we need for a complete rollback.