diff --git a/.packit.yaml b/.packit.yaml index 31a55d391..a4c0c5e4f 100644 --- a/.packit.yaml +++ b/.packit.yaml @@ -40,7 +40,7 @@ jobs: # TEST JOBS ## Tests on pull request stage -- &tests-tier0 +- &tests-tier0-destructive-manual job: tests # Run tests on-demand manual_trigger: true @@ -56,8 +56,8 @@ jobs: # to tag the updated system correctly distros: [centos-8-latest, oraclelinux-8.6] trigger: pull_request - identifier: "tier0" - tmt_plan: "tier0" + identifier: "tier0-destructive" + tmt_plan: "tier0/destructive" # Run on Red Testing Farm Hat Ranch, tag resources to sst_conversions use_internal_tf: True tf_extra_params: @@ -68,33 +68,39 @@ jobs: BusinessUnit: sst_conversions labels: - tier0 + - tier0-destructive + - destructive - &tests-tier0-non-destructive-manual - <<: *tests-tier0 + <<: *tests-tier0-destructive-manual identifier: "tier0-non-destructive" tmt_plan: "tier0/non-destructive" labels: + - tier0 - tier0-non-destructive - non-destructive -- &tests-tier1-manual - <<: *tests-tier0 - identifier: "tier1" - tmt_plan: "tier1" +- &tests-tier1-destructive-manual + <<: *tests-tier0-destructive-manual + identifier: "tier1-destructive" + tmt_plan: "tier1/destructive" labels: - tier1 + - tier1-destructive + - destructive -- &tests-tier1-non-destructive-manual - <<: *tests-tier0 - identifier: "tier1-non-destructive" - tmt_plan: "tier1/non-destructive" - labels: - - tier1-non-destructive - - non-destructive +# Disabled for now +#- &tests-tier1-non-destructive-manual +# <<: *tests-tier0-destructive-manual +# identifier: "tier1-non-destructive" +# tmt_plan: "tier1/non-destructive" +# labels: +# - tier1-non-destructive +# - non-destructive # Tests on merge to main stage - &tests-tier1 - <<: *tests-tier0 + <<: *tests-tier0-destructive-manual # Run test automatically with merge commit to main branch manual_trigger: false identifier: "tier1" diff --git a/convert2rhel/actions/__init__.py b/convert2rhel/actions/__init__.py index 009dbebe5..6f9bc641f 100644 --- a/convert2rhel/actions/__init__.py +++ b/convert2rhel/actions/__init__.py @@ -22,7 +22,6 @@ import itertools import logging import pkgutil -import re import traceback from functools import wraps @@ -48,6 +47,7 @@ #: and start to use it with console.redhat.com #: #: :SUCCESS: no problem. +#: :INFO: informational message to the user, no problem. #: :WARNING: the problem is just a warning displayed to the user. (unused, #: warnings are currently emitted directly from the Action). #: :SKIP: the action could not be run because a dependent Action failed. @@ -67,6 +67,7 @@ #: runs the Actions will set this. STATUS_CODE = { "SUCCESS": 0, + "INFO": 25, "WARNING": 51, "SKIP": 101, "OVERRIDABLE": 152, @@ -81,6 +82,7 @@ #: what the results mean _STATUS_HEADER = { 0: "Success (No changes needed)", + 25: "Info (No changes needed)", 51: "Warning (Review and fix if needed)", 101: "Skip (Could not be checked due to other failures)", 152: "Overridable (Review and either fix or ignore the failure)", @@ -104,17 +106,13 @@ def wrapper(self, *args, **kwargs): return_value = func(self, *args, **kwargs) if self.result is None: - self.result = ActionResult("SUCCESS") + self.result = ActionResult(level="SUCCESS", id="SUCCESS") return return_value return wrapper -#: Used as a sentinel value for Action.set_result() method. -_NO_USER_VALUE = object() - - class ActionError(Exception): """Raised for errors related to the Action framework.""" @@ -227,23 +225,29 @@ def result(self, action_message): self._result = action_message - def set_result(self, level, id=None, message=None): + def set_result(self, level, id, title=None, description=None, diagnosis=None, remediation=None): """ - Helper method that sets the resulting values for level, id and message. + Helper method that sets the resulting values for level, id, title, description, diagnosis and remediation. :param id: The id to identify the result. :type id: str - :param level: The status_code of the result . + :param level: The status_code of the result. :type: level: str - :param message: The message to be set. - :type message: str | None + :param title: The title to be set. + :type title: str + :param description: The description of the result. + :type description: str + :param diagnosis: The outline of the issue found. + :type diagnosis: str | None + :param remediation: The steps that can be taken to resolve the issue. + :type remediation: str | None """ if level not in ("ERROR", "OVERRIDABLE", "SKIP", "SUCCESS"): raise KeyError("The level of result must be FAILURE, OVERRIDABLE, SKIP, or SUCCESS.") - self.result = ActionResult(level, id, message) + self.result = ActionResult(level, id, title, description, diagnosis, remediation) - def add_message(self, level, id=None, message=None): + def add_message(self, level, id, title=None, description=None, diagnosis=None, remediation=None): """ Helper method that adds a new informational message to display to the user. The passed in values for level, id and message of a warning or info log message are @@ -253,10 +257,16 @@ def add_message(self, level, id=None, message=None): :type id: str :param level: The level to be set. :type: level: str - :param message: The message to be set. - :type message: str | None + :param title: The title to be set. + :type title: str + :param description: The description of the message. + :type description: str + :param diagnosis: The outline of the issue found. + :type diagnosis: str | None + :param remediation: The steps that can be taken to resolve the issue. + :type remediation: str | None """ - msg = ActionMessage(level, id, message) + msg = ActionMessage(level, id, title, description, diagnosis, remediation) self.messages.append(msg) @@ -269,14 +279,42 @@ class ActionMessageBase: :type id: str :keyword level: The level to be set, defaulting to SUCCESS. :type: level: str - :keyword message: The message to be set. - :type message: str | None + :keyword title: The title to be set. + :type title: str + :keyword description: The description to be set. + :type description: str + :keyword diagnosis: The diagnosis to be set. + :type diagnosis: str | None + :keyword remediation: The remediation to be set. + :type remediation: str | None """ - def __init__(self, level="SUCCESS", id=None, message=""): + def __init__(self, level="SUCCESS", id="SUCCESS", title="", description="", diagnosis="", remediation=""): self.id = id self.level = STATUS_CODE[level] - self.message = message + self.title = title + self.description = description + self.diagnosis = diagnosis + self.remediation = remediation + + def __eq__(self, other): + if hash(self) == hash(other): + return True + return False + + def __hash__(self): + return hash((self.level, self.id, self.title, self.description, self.diagnosis, self.remediation)) + + def __repr__(self): + return "%s(level=%s, id=%s, title=%s, description=%s, diagnosis=%s, remediation=%s)" % ( + self.__class__.__name__, + _STATUS_NAME_FROM_CODE[self.level], + self.id, + self.title, + self.description, + self.diagnosis, + self.remediation, + ) def to_dict(self): """ @@ -287,7 +325,10 @@ def to_dict(self): return { "id": self.id, "level": self.level, - "message": self.message, + "title": self.title, + "description": self.description, + "diagnosis": self.diagnosis, + "remediation": self.remediation, } @@ -296,16 +337,16 @@ class ActionMessage(ActionMessageBase): A class that defines the contents and rules for messages set through :meth:`Action.add_message`. """ - def __init__(self, level=None, id=None, message=None): - if not (id and level and message): - raise InvalidMessageError("Messages require id, level and message fields") + def __init__(self, level=None, id=None, title=None, description=None, diagnosis="", remediation=""): + if not (id and level and title and description): + raise InvalidMessageError("Messages require id, level, title and description fields") # None of the result status codes are legal as a message. So we error if any # of them were given here. if not (STATUS_CODE["SUCCESS"] < STATUS_CODE[level] < STATUS_CODE["SKIP"]): raise InvalidMessageError("Invalid level '%s', set for a non-result message" % level) - super(ActionMessage, self).__init__(level, id, message) + super(ActionMessage, self).__init__(level, id, title, description, diagnosis, remediation) class ActionResult(ActionMessageBase): @@ -313,24 +354,21 @@ class ActionResult(ActionMessageBase): A class that defines content and rules for messages set through :meth:`Action.set_result`. """ - def __init__(self, level="SUCCESS", id=None, message=""): + def __init__(self, level="SUCCESS", id="SUCCESS", title="", description="", diagnosis="", remediation=""): + if not id: + raise InvalidMessageError("Results require the id field") if STATUS_CODE[level] >= STATUS_CODE["SKIP"]: - if not id and not message: - raise InvalidMessageError("Non-success results require an id and a message") - - if not id: - raise InvalidMessageError("Non-success results require an id") - - if not message: - raise InvalidMessageError("Non-success results require a message") + if not (level and title and description): + # id is placed in the error message so it is less confusing for the user + raise InvalidMessageError("Non-success results require level, title and description fields") elif STATUS_CODE["SUCCESS"] < STATUS_CODE[level] < STATUS_CODE["SKIP"]: raise InvalidMessageError( "Invalid level '%s', the level for result must be SKIP or more fatal or SUCCESS." % level ) - super(ActionResult, self).__init__(level, id, message) + super(ActionResult, self).__init__(level, id, title, description, diagnosis, remediation) def get_actions(actions_path, prefix): @@ -483,15 +521,15 @@ def run(self, successes=None, failures=None, skips=None): to_be = "was" if len(failed_deps) > 1: to_be = "were" - message = "Skipped because %s %s not successful" % ( + description = "Skipped because %s %s not successful" % ( utils.format_sequence_as_message(failed_deps), to_be, ) - action.set_result(level="SKIP", id="SKIP", message=message) + action.set_result(level="SKIP", id="SKIP", title="Skipped action", description=description) skips.append(action) failed_action_ids.add(action.id) - logger.error("Skipped %s. %s" % (action.id, message)) + logger.error("Skipped %s. %s" % (action.id, description)) continue # Run the Action @@ -500,13 +538,15 @@ def run(self, successes=None, failures=None, skips=None): except (Exception, SystemExit) as e: # Uncaught exceptions are handled by constructing a generic # failure message here that should be reported - message = ( + description = ( "Unhandled exception was caught: %s\n" "Please file a bug at https://issues.redhat.com/ to have this" " fixed or a specific error message added.\n" "Traceback: %s" % (e, traceback.format_exc()) ) - action.set_result(level="ERROR", id="UNEXPECTED_ERROR", message=message) + action.set_result( + level="ERROR", id="UNEXPECTED_ERROR", title="Unhandled exception caught", description=description + ) # Categorize the results if action.result.level <= STATUS_CODE["WARNING"]: @@ -515,7 +555,7 @@ def run(self, successes=None, failures=None, skips=None): if action.result.level > STATUS_CODE["WARNING"]: message = format_action_status_message( - action.result.level, action.id, action.result.id, action.result.message + action.result.level, action.id, action.result.id, action.result.to_dict() ) logger.error(message) failures.append(action) @@ -699,7 +739,7 @@ def find_actions_of_severity(results, severity, key): return matched_actions -def format_action_status_message(status_code, action_id, id, message): +def format_action_status_message(status_code, action_id, id, result): """Helper function to format a message about each Action result. :param status_code: The status code that will be used in the template. @@ -708,31 +748,42 @@ def format_action_status_message(status_code, action_id, id, message): :type action_id: str :param id: Error id associated with the action :type id: str - :param message: The message that was produced in the action - :type message: str + :param result: The result that was produced in the action + :type result: dict[str, Any] :return: The formatted message that will be logged to the user. :rtype: str """ level_name = _STATUS_NAME_FROM_CODE[status_code] - template = "({LEVEL}) {ACTION_ID}" - # `id` and `message` may not be present everytime, since it - # can be empty (either by mistake, or, intended), we only want to - # apply these fields if they are present, with a special mention to - # `message`. - if id: - template += "::{ID}" - - # Special case for `message` to not output empty message to the - # user without message. - if message: - template += " - {MESSAGE}" - else: - template += " - [No further information given]" + template = "({LEVEL}) {ACTION_ID}::{ID} -" + default_message = "[No further information given]" + + # Success results doesn't need to have id, title or anything else. Instead, + # we can output a simple message with the addition of the `No further + # information given` and return earlier to skip the other conditionals + # checks. + if status_code == STATUS_CODE["SUCCESS"]: + template += " {MESSAGE}" + return template.format(ID=id, LEVEL=level_name, ACTION_ID=action_id, MESSAGE=default_message) + + title = result["title"] + template += " {TITLE}\n" + + description = result["description"] if result["description"] else default_message + template += " Description: {DESCRIPTION}\n" + + diagnosis = result["diagnosis"] if result["diagnosis"] else default_message + template += " Diagnosis: {DIAGNOSIS}\n" + + remediation = result["remediation"] if result["remediation"] else default_message + template += " Remediation: {REMEDIATION}\n" return template.format( LEVEL=level_name, ACTION_ID=action_id, ID=id, - MESSAGE=message, + TITLE=title, + DESCRIPTION=description, + DIAGNOSIS=diagnosis, + REMEDIATION=remediation, ) diff --git a/convert2rhel/actions/pre_ponr_changes/backup_system.py b/convert2rhel/actions/pre_ponr_changes/backup_system.py index ef450d5b7..0f8d720b9 100644 --- a/convert2rhel/actions/pre_ponr_changes/backup_system.py +++ b/convert2rhel/actions/pre_ponr_changes/backup_system.py @@ -45,7 +45,9 @@ def run(self): # Raised in module redhatrelease on lines 49 and 60 # - If unable to find the /etc/system-release file, # SystemExit is raised - self.set_result(level="ERROR", id="UNKNOWN_ERROR", message=str(e)) + self.set_result( + level="ERROR", id="UNKNOWN_ERROR", title="An unknown error has occurred", description=str(e) + ) class BackupRepository(actions.Action): diff --git a/convert2rhel/actions/pre_ponr_changes/handle_packages.py b/convert2rhel/actions/pre_ponr_changes/handle_packages.py index 654913359..e4e06d508 100644 --- a/convert2rhel/actions/pre_ponr_changes/handle_packages.py +++ b/convert2rhel/actions/pre_ponr_changes/handle_packages.py @@ -24,6 +24,10 @@ logger = logging.getLogger(__name__) +def _get_pkg_names(packages): + return [pkghandler.get_pkg_nevra(pkg_obj, include_zero_epoch=True) for pkg_obj in packages] + + class ListThirdPartyPackages(actions.Action): id = "LIST_THIRD_PARTY_PACKAGES" @@ -37,15 +41,35 @@ def run(self): logger.task("Convert: List third-party packages") third_party_pkgs = pkghandler.get_third_party_pkgs() if third_party_pkgs: - logger.warning( + pkg_list = pkghandler.format_pkg_info(sorted(third_party_pkgs, key=self.extract_packages)) + warning_message = ( "Only packages signed by %s are to be" " replaced. Red Hat support won't be provided" " for the following third party packages:\n" % system_info.name ) - pkghandler.print_pkg_info(third_party_pkgs) + + logger.warning(warning_message) + logger.info(pkg_list) + self.set_result( + level="SUCCESS", + id="THIRD_PARTY_PACKAGE_DETECTED", + title="Third party packages detected", + ) + self.add_message( + level="WARNING", + id="THIRD_PARTY_PACKAGE_DETECTED_MESSAGE", + title="Third party packages detected", + description="Third party packages will not be replaced during the conversion.", + diagnosis=warning_message + ", ".join(_get_pkg_names(third_party_pkgs)), + ) + return else: logger.info("No third party packages installed.") + def extract_packages(self, pkg): + """Key function to extract the package name from third_party_pkgs""" + return pkg.nevra.name + class RemoveExcludedPackages(actions.Action): id = "REMOVE_EXCLUDED_PACKAGES" @@ -60,8 +84,9 @@ def run(self): logger.task("Convert: Remove excluded packages") logger.info("Searching for the following excluded packages:\n") try: - pkgs_to_remove = pkghandler._get_packages_to_remove(system_info.excluded_pkgs) - pkghandler.remove_pkgs_unless_from_redhat(pkgs_to_remove) + 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) or []) # TODO: Handling SystemExit here as way to speedup exception # handling and not refactor contents of the underlying function. @@ -69,7 +94,38 @@ def run(self): # 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="PACKAGE_REMOVAL_FAILED", message=str(e)) + self.set_result( + 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=str(e), + ) + return + + # shows which packages were not removed, if false, all packages were removed + pkgs_not_removed = sorted(frozenset(pkgs_to_remove).difference(pkgs_removed)) + if pkgs_not_removed: + pkg_names = _get_pkg_names(pkgs_not_removed) + message = "The following packages were not removed: %s" % ", ".join(pkg_names) + 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", + diagnosis=message, + ) + else: + message = "The following packages were removed: %s" % ", ".join(pkgs_removed) + logger.info(message) + self.add_message( + level="INFO", + id="EXCLUDED_PACKAGES_REMOVED", + title="Excluded packages removed", + description="Excluded packages that have been removed", + diagnosis=message, + ) class RemoveRepositoryFilesPackages(actions.Action): @@ -96,8 +152,9 @@ def run(self): logger.task("Convert: Remove packages containing .repo files") logger.info("Searching for packages containing .repo files or affecting variables in the .repo files:\n") try: - pkgs_to_remove = pkghandler._get_packages_to_remove(system_info.repofile_pkgs) - pkghandler.remove_pkgs_unless_from_redhat(pkgs_to_remove) + 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) or []) # TODO: Handling SystemExit here as way to speedup exception # handling and not refactor contents of the underlying function. @@ -105,4 +162,35 @@ def run(self): # 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="PACKAGE_REMOVAL_FAILED", message=str(e)) + 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 + + # shows which packages were not removed, if false, all packages were removed + pkgs_not_removed = sorted(frozenset(pkgs_to_remove).difference(pkgs_removed)) + if pkgs_not_removed: + pkg_names = _get_pkg_names(pkgs_not_removed) + message = "The following packages were not removed: %s" % ", ".join(pkg_names) + 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, + ) + else: + message = "The following packages were removed: %s" % ", ".join(pkgs_removed) + logger.info(message) + self.add_message( + level="INFO", + id="REPOSITORY_FILE_PACKAGES_REMOVED", + title="Repository file packages removed", + description="Repository file packages that were removed", + diagnosis=message, + ) diff --git a/convert2rhel/actions/pre_ponr_changes/kernel_modules.py b/convert2rhel/actions/pre_ponr_changes/kernel_modules.py index bc65e02f2..0a9130d1c 100644 --- a/convert2rhel/actions/pre_ponr_changes/kernel_modules.py +++ b/convert2rhel/actions/pre_ponr_changes/kernel_modules.py @@ -215,33 +215,50 @@ def run(self): " We will continue the conversion with the following kernel modules unavailable in RHEL:\n" "{kmods}\n".format(kmods="\n".join(unsupported_kmods)) ) + self.add_message( + level="WARNING", + id="ALLOW_UNAVAILABLE_KERNEL_MODULES", + title="Skipping the ensure kernel modules compatibility check", + description="Detected 'CONVERT2RHEL_ALLOW_UNAVAILABLE_KMODS' environment variable.", + diagnosis="We will continue the conversion with the following kernel modules unavailable in RHEL:\n" + "{kmods}\n".format(kmods="\n".join(unsupported_kmods)), + ) return - # If there is any unsupported kmods found, set the result to error + # If there is any unsupported kmods found, set the result to overridable if unsupported_kmods: self.set_result( - level="ERROR", + level="OVERRIDABLE", id="UNSUPPORTED_KERNEL_MODULES", - message=( - "The following loaded kernel modules are not available in RHEL:\n{0}\n" - "Ensure you have updated the kernel to the latest available version and rebooted the system.\nIf this " - "message persists, you can prevent the modules from loading by following {1} and rerun convert2rhel.\n" - "Keeping them loaded could cause the system to malfunction after the conversion as they might not work " - "properly with the RHEL kernel.\n" - "To circumvent this check and accept the risk you can set environment variable " - "'CONVERT2RHEL_ALLOW_UNAVAILABLE_KMODS=1'.".format( - "\n".join(unsupported_kmods), LINK_PREVENT_KMODS_FROM_LOADING - ) + title="Unsupported kernel modules", + description="Unsupported kernel modules were found", + diagnosis="The following loaded kernel modules are not available in RHEL:\n{0}\n".format( + "\n".join(unsupported_kmods) ), + remediation="Ensure you have updated the kernel to the latest available version and rebooted the system.\nIf this " + "message persists, you can prevent the modules from loading by following {0} and rerun convert2rhel.\n" + "Keeping them loaded could cause the system to malfunction after the conversion as they might not work " + "properly with the RHEL kernel.\n" + "To circumvent this check and accept the risk you can set environment variable " + "'CONVERT2RHEL_ALLOW_UNAVAILABLE_KMODS=1'.".format(LINK_PREVENT_KMODS_FROM_LOADING), ) return logger.debug("All loaded kernel modules are available in RHEL.") except RHELKernelModuleNotFound as e: - self.set_result(level="ERROR", id="NO_RHEL_KERNEL_MODULES_FOUND", message=str(e)) + self.set_result( + level="ERROR", + id="NO_RHEL_KERNEL_MODULES_FOUND", + title="No RHEL kernel modules were found", + description="This check was unable to find any kernel modules in the packages in the enabled yum repositories.", + diagnosis=str(e), + remediation="Adding additional repositories to those mentioned in the diagnosis may solve this issue.", + ) except ValueError as e: self.set_result( level="ERROR", id="CANNOT_COMPARE_PACKAGE_VERSIONS", - message="Package comparison failed: %s" % e, + title="Error while comparing packages", + description="There was an error while detecting the kernel package which corresponds to the kernel modules present on the system.", + diagnosis="Package comparison failed: %s" % e, ) diff --git a/convert2rhel/actions/pre_ponr_changes/special_cases.py b/convert2rhel/actions/pre_ponr_changes/special_cases.py index d459bf848..e1c0d8f9a 100644 --- a/convert2rhel/actions/pre_ponr_changes/special_cases.py +++ b/convert2rhel/actions/pre_ponr_changes/special_cases.py @@ -63,6 +63,13 @@ def run(self): _, exit_code = run_subprocess(["rpm", "-e", "--nodeps", "iwlax2xx-firmware"]) if exit_code != 0: logger.error("Unable to remove the package iwlax2xx-firmware.") + self.add_message( + level="WARNING", + id="IWLAX2XX_FIRMWARE_REMOVAL_FAILED", + title="Package removal failed", + description="Unable to remove the package iwlax2xx-firmware.", + ) + else: logger.info( "The iwl7260-firmware and iwlax2xx-firmware packages are not both installed. Nothing to do." diff --git a/convert2rhel/actions/pre_ponr_changes/subscription.py b/convert2rhel/actions/pre_ponr_changes/subscription.py index 032b01f57..49056cd79 100644 --- a/convert2rhel/actions/pre_ponr_changes/subscription.py +++ b/convert2rhel/actions/pre_ponr_changes/subscription.py @@ -32,6 +32,12 @@ def run(self): if toolopts.tool_opts.no_rhsm: logger.warning("Detected --no-rhsm option. Skipping.") + self.add_message( + level="WARNING", + id="PRE_SUBSCRIPTION_CHECK_SKIP", + title="Pre-subscription check skip", + description="Detected --no-rhsm option. Skipping.", + ) return try: @@ -67,12 +73,21 @@ def run(self): # TODO(r0x0d): This should be refactored to handle each case # individually rather than relying on SystemExit. - self.set_result(level="ERROR", id="UNKNOWN_ERROR", message=str(e)) + self.set_result( + level="ERROR", + id="UNKNOWN_ERROR", + title="Unknown error", + description="The cause of this error is unknown, please look at the diagnosis for more information.", + diagnosis=str(e), + ) except subscription.UnregisterError as e: self.set_result( level="ERROR", id="UNABLE_TO_REGISTER", - message="Failed to unregister the system: %s" % e, + title="System unregistration failure", + description="The system is already registered with subscription-manager even though it is running CentOS not RHEL. We have failed to remove that registration.", + diagnosis="Failed to unregister the system: %s" % e, + remediation="You may want to unregister the system manually and re-run convert2rhel.", ) @@ -89,6 +104,12 @@ def run(self): if toolopts.tool_opts.no_rhsm: logger.warning("Detected --no-rhsm option. Skipping.") + self.add_message( + level="WARNING", + id="SUBSCRIPTION_CHECK_SKIP", + title="Subscription check skip", + description="Detected --no-rhsm option. Skipping.", + ) return try: @@ -98,9 +119,6 @@ def run(self): logger.task("Convert: Get RHEL repository IDs") rhel_repoids = repo.get_rhel_repoids() - logger.task("Convert: Subscription Manager - Check required repositories") - subscription.check_needed_repos_availability(rhel_repoids) - logger.task("Convert: Subscription Manager - Disable all repositories") subscription.disable_repos() @@ -117,7 +135,9 @@ def run(self): self.set_result( level="ERROR", id="MISSING_SUBSCRIPTION_MANAGER_BINARY", - message="Failed to execute command: %s" % e, + title="Missing subscription-manager binary", + description="There is a missing subscription-manager binary", + diagnosis="Failed to execute command: %s" % e, ) except SystemExit as e: # TODO(r0x0d): This should be refactored to handle each case @@ -128,10 +148,18 @@ def run(self): # - Maximum sub-man retries reached # - If the return-code is different from 0 in disabling repos, # SystemExit is raised. - self.set_result(level="ERROR", id="UNKNOWN_ERROR", message=str(e)) + self.set_result( + level="ERROR", + id="UNKNOWN_ERROR", + title="Unknown error", + description="The cause of this error is unknown, please look at the diagnosis for more information.", + diagnosis=str(e), + ) except ValueError as e: self.set_result( level="ERROR", id="MISSING_REGISTRATION_COMBINATION", - message="One or more combinations were missing for subscription-manager parameters: %s" % e, + title="Missing registration combination", + description="There are missing registration combinations", + diagnosis="One or more combinations were missing for subscription-manager parameters: %s" % str(e), ) diff --git a/convert2rhel/actions/pre_ponr_changes/transaction.py b/convert2rhel/actions/pre_ponr_changes/transaction.py index f61ae4978..fe9986eff 100644 --- a/convert2rhel/actions/pre_ponr_changes/transaction.py +++ b/convert2rhel/actions/pre_ponr_changes/transaction.py @@ -65,4 +65,10 @@ def run(self): # - If we fail to resolve dependencies in the transaction # - If we fail to download the transaction packages # - If we fail to validate the transaction - self.set_result(level="ERROR", id="UNKNOWN_ERROR", message=str(e)) + self.set_result( + level="ERROR", + id="UNKNOWN_ERROR", + title="Unknown", + description="The cause of this error is unknown, please look at the diagnosis for more information.", + diagnosis=str(e), + ) diff --git a/convert2rhel/actions/report.py b/convert2rhel/actions/report.py index a5ca93ac1..76846304d 100644 --- a/convert2rhel/actions/report.py +++ b/convert2rhel/actions/report.py @@ -42,6 +42,8 @@ _STATUS_TO_COLOR = { # SUCCESS 0: "OKGREEN", + # INFO + 25: "INFO", # WARNING 51: "WARNING", # SKIP @@ -89,6 +91,33 @@ def summary_as_json(results, json_file=CONVERT2RHEL_JSON_RESULTS): json.dump(envelope, f) +def wrap_paragraphs(text, width=70, **kwargs): + """ + Wrap the paragraphs for a given text respecting the line breaks defined in + the string (if any). + + This solution was taken from + https://github.com/python/cpython/issues/46167#issuecomment-1093406764, + which is a solution to textwrap not properly respecting line breaks inside + strings. + """ + output = [] + first = True + indent = "" + subsequent_indent = " " + for paragraph in text.splitlines(): + for line in textwrap.wrap( + paragraph, width, initial_indent=indent, subsequent_indent=subsequent_indent, **kwargs + ) or [""]: + output.append(line) + if first: + indent = subsequent_indent + subsequent_indent = "" + first = False + + return "\n".join(output) + + def summary(results, include_all_reports=False, with_colors=True): """Output a summary regarding the actions execution. @@ -146,12 +175,18 @@ def summary(results, include_all_reports=False, with_colors=True): for action_id, action_value in results.items(): combined_results_and_message[(action_id, action_value["result"]["id"])] = { "level": action_value["result"]["level"], - "message": action_value["result"]["message"], + "title": action_value["result"]["title"], + "description": action_value["result"]["description"], + "remediation": action_value["result"]["remediation"], + "diagnosis": action_value["result"]["diagnosis"], } for message in action_value["messages"]: combined_results_and_message[(action_id, message["id"])] = { "level": message["level"], - "message": message["message"], + "title": message["title"], + "description": message["description"], + "remediation": message["remediation"], + "diagnosis": message["diagnosis"], } if include_all_reports: @@ -174,10 +209,8 @@ def summary(results, include_all_reports=False, with_colors=True): report.append(format_report_section_heading(combined_result["level"])) last_level = combined_result["level"] - entry = format_action_status_message( - combined_result["level"], message_id[0], message_id[1], combined_result["message"] - ) - entry = word_wrapper.fill(entry) + entry = format_action_status_message(combined_result["level"], message_id[0], message_id[1], combined_result) + entry = wrap_paragraphs(entry, width=terminal_size[0]) if with_colors: entry = colorize(entry, _STATUS_TO_COLOR[combined_result["level"]]) report.append(entry) diff --git a/convert2rhel/actions/system_checks/convert2rhel_latest.py b/convert2rhel/actions/system_checks/convert2rhel_latest.py index a8ff111ac..a6a2478e6 100644 --- a/convert2rhel/actions/system_checks/convert2rhel_latest.py +++ b/convert2rhel/actions/system_checks/convert2rhel_latest.py @@ -59,7 +59,14 @@ def run(self): super(Convert2rhelLatest, self).run() if not system_info.has_internet_access: - logger.warning("Skipping the check because no internet connection has been detected.") + description = "Skipping the check because no internet connection has been detected." + logger.warning(description) + self.add_message( + level="WARNING", + id="CONVERT2RHEL_LATEST_CHECK_SKIP_NO_INTERNET", + title="Skipping convert2rhel latest version check", + description=description, + ) return repo_dir = tempfile.mkdtemp(prefix="convert2rhel_repo.", dir=utils.TMP_DIR) @@ -87,10 +94,18 @@ def run(self): shutil.rmtree(repo_dir) if return_code != 0: - logger.warning( - "Couldn't check if the current installed Convert2RHEL is the latest version.\n" + diagnosis = ( + "Couldn't check if the current installed convert2rhel is the latest version.\n" "repoquery failed with the following output:\n%s" % (raw_output_convert2rhel_versions) ) + logger.warning(diagnosis) + self.add_message( + level="WARNING", + id="CONVERT2RHEL_LATEST_CHECK_SKIP", + title="convert2rhel latest version check skip", + description="Skipping the convert2hel latest version check", + diagnosis=diagnosis, + ) return # convert the raw output of convert2rhel version strings into a list @@ -147,7 +162,7 @@ def run(self): logger.debug("Found %s to be latest available version" % (latest_available_version[1])) # After the for loop, the latest_available_version variable will gain the epoch, version, and release - # (e.g. ("0" "0.26" "1.el7")) information from the Convert2RHEL yum repo + # (e.g. ("0" "0.26" "1.el7")) information from the convert2rhel yum repo # when the versions are the same the latest_available_version's release field will cause it to evaluate as a later version. # Therefore we need to hardcode "0" for both the epoch and release below for installed_convert2rhel_version # and latest_available_version respectively, to compare **just** the version field. @@ -164,32 +179,60 @@ def run(self): " environment variable. Please switch to 'CONVERT2RHEL_ALLOW_OLDER_VERSION'" " instead." ) + self.add_message( + level="WARNING", + id="DEPRECATED_ENVIRONMENT_VARIABLE", + title="Deprecated environment variable", + description="A deprecated environment variable has been detected", + diagnosis="You are using the deprecated 'CONVERT2RHEL_UNSUPPORTED_VERSION'", + remediation="Please switch to the 'CONVERT2RHEL_ALLOW_OLDER_VERSION' environment variable instead", + ) - logger.warning( - "You are currently running %s and the latest version of Convert2RHEL is %s.\n" + diagnosis = ( + "You are currently running %s and the latest version of convert2rhel is %s.\n" "'CONVERT2RHEL_ALLOW_OLDER_VERSION' environment variable detected, continuing conversion" % (installed_convert2rhel_version, latest_available_version[1]) ) - + logger.warning(diagnosis) + self.add_message( + level="WARNING", + id="ALLOW_OLDER_VERSION_ENVIRONMENT_VARIABLE", + title="Outdated convert2rhel version detected", + description="An outdated convert2rhel version has been detected", + diagnosis=diagnosis, + ) else: if int(system_info.version.major) <= 6: logger.warning( - "You are currently running %s and the latest version of Convert2RHEL is %s.\n" + "You are currently running %s and the latest version of convert2rhel is %s.\n" "We encourage you to update to the latest version." % (installed_convert2rhel_version, latest_available_version[1]) ) + self.add_message( + level="WARNING", + id="OUTDATED_CONVERT2RHEL_VERSION", + title="Outdated convert2rhel version detected", + description="An outdated convert2rhel version has been detected", + diagnosis=( + "You are currently running %s and the latest version of convert2rhel is %s.\n" + "We encourage you to update to the latest version." + % (installed_convert2rhel_version, latest_available_version[1]) + ), + ) else: self.set_result( level="ERROR", id="OUT_OF_DATE", - message=( - "You are currently running %s and the latest version of Convert2RHEL is %s.\n" - "Only the latest version is supported for conversion. If you want to ignore" - " this check, then set the environment variable 'CONVERT2RHEL_ALLOW_OLDER_VERSION=1' to continue." + title="Outdated convert2rhel version detected", + description="An outdated convert2rhel version has been detected", + diagnosis=( + "You are currently running %s and the latest version of convert2rhel is %s.\n" + "Only the latest version is supported for conversion." % (installed_convert2rhel_version, latest_available_version[1]) ), + remediation="If you want to ignore this check, then set the environment variable 'CONVERT2RHEL_ALLOW_OLDER_VERSION=1' to continue.", ) return - logger.info("Latest available Convert2RHEL version is installed.") + logger.info("Latest available convert2rhel version is installed.") diff --git a/convert2rhel/actions/system_checks/custom_repos_are_valid.py b/convert2rhel/actions/system_checks/custom_repos_are_valid.py index fee771448..9c2729b43 100644 --- a/convert2rhel/actions/system_checks/custom_repos_are_valid.py +++ b/convert2rhel/actions/system_checks/custom_repos_are_valid.py @@ -38,7 +38,14 @@ def run(self): logger.task("Prepare: Check if --enablerepo repositories are accessible") if not tool_opts.no_rhsm: - logger.info("Skipping the check of repositories due to the use of RHSM for the conversion.") + description = "Skipping the check of repositories due to the use of RHSM for the conversion." + logger.info(description) + self.add_message( + level="INFO", + id="CUSTOM_REPOSITORIES_ARE_VALID_CHECK_SKIP", + title="Skipping the custom repos are valid check", + description=description, + ) return output, ret_code = call_yum_cmd( @@ -50,10 +57,10 @@ def run(self): self.set_result( level="ERROR", id="UNABLE_TO_ACCESS_REPOSITORIES", - message=( - "Unable to access the repositories passed through the --enablerepo option. " - "For more details, see YUM/DNF output:\n{0}".format(output) - ), + title="Unable to access repositories", + description="Access could not be made to the custom repositories.", + diagnosis="Unable to access the repositories passed through the --enablerepo option.", + remediation="For more details, see YUM/DNF output:\n{0}".format(output), ) return diff --git a/convert2rhel/actions/system_checks/dbus.py b/convert2rhel/actions/system_checks/dbus.py index ba22741d1..cd4695880 100644 --- a/convert2rhel/actions/system_checks/dbus.py +++ b/convert2rhel/actions/system_checks/dbus.py @@ -35,6 +35,12 @@ def run(self): if tool_opts.no_rhsm: logger.info("Skipping the check because we have been asked not to subscribe this system to RHSM.") + self.add_message( + level="WARNING", + id="DBUS_IS_RUNNING_CHECK_SKIP", + title="Skipping the dbus is running check", + description="Skipping the check because we have been asked not to subscribe this system to RHSM.", + ) return if system_info.dbus_running: @@ -44,8 +50,8 @@ def run(self): self.set_result( level="ERROR", id="DBUS_DAEMON_NOT_RUNNING", - message=( - "Could not find a running DBus Daemon which is needed to register with subscription manager.\n" - "Please start dbus using `systemctl start dbus`" - ), + title="Dbus daemon not running", + description="The Dbus daemon is not running", + diagnosis="Could not find a running DBus Daemon which is needed to register with subscription manager.", + remediation="Please start dbus using `systemctl start dbus`", ) diff --git a/convert2rhel/actions/system_checks/efi.py b/convert2rhel/actions/system_checks/efi.py index 3a40b52b5..fc48bac8c 100644 --- a/convert2rhel/actions/system_checks/efi.py +++ b/convert2rhel/actions/system_checks/efi.py @@ -41,14 +41,18 @@ def run(self): self.set_result( level="ERROR", id="NON_x86_64", - message="Only x86_64 systems are supported for UEFI conversions.", + title="None x86_64 system detected", + description="Only x86_64 systems are supported for UEFI conversions.", ) return if not os.path.exists("/usr/sbin/efibootmgr"): self.set_result( level="ERROR", id="EFIBOOTMGR_NOT_FOUND", - message="Install efibootmgr to continue converting the UEFI-based system.", + title="EFI boot manager not found", + description="The EFI boot manager could not be found.", + diagnosis="The EFI boot manager tool - efibootmgr could not be found on your system", + remediation="Install efibootmgr to continue converting the UEFI-based system.", ) return if grub.is_secure_boot(): @@ -56,19 +60,25 @@ def run(self): self.set_result( level="ERROR", id="SECURE_BOOT_DETECTED", - message=( - "The conversion with secure boot is currently not possible.\n" - "To disable it, follow the instructions available in this article: https://access.redhat.com/solutions/6753681" - ), + title="Secure boot detected", + description="Secure boot has been detected.", + diagnosis="The conversion with secure boot is currently not possible.", + remediation="To disable secure boot, follow the instructions available in this article: https://access.redhat.com/solutions/6753681", ) return - # Get information about the bootloader. Currently the data is not used, but it's + # Get information about the bootloader. Currently, the data is not used, but it's # good to check that we can obtain all the required data before the PONR. try: efiboot_info = grub.EFIBootInfo() except grub.BootloaderError as e: - self.set_result(level="ERROR", id="BOOTLOADER_ERROR", message=str(e)) + self.set_result( + level="ERROR", + id="BOOTLOADER_ERROR", + title="Bootloader error detected", + description="An unknown bootloader error occurred, please look at the diagnosis for more information.", + diagnosis=str(e), + ) return if not efiboot_info.entries[efiboot_info.current_bootnum].is_referring_to_file(): @@ -78,6 +88,16 @@ def run(self): "The current UEFI bootloader '%s' is not referring to any binary UEFI" " file located on local EFI System Partition (ESP)." % efiboot_info.current_bootnum ) + self.add_message( + level="WARNING", + id="UEFI_BOOTLOADER_MISMATCH", + title="UEFI bootloader mismatch", + description="There was a UEFI bootloader mismatch.", + diagnosis=( + "The current UEFI bootloader '%s' is not referring to any binary UEFI" + " file located on local EFI System Partition (ESP)." % efiboot_info.current_bootnum + ), + ) # TODO(pstodulk): print warning when multiple orig. UEFI entries point # to the original system (e.g. into the centos/ directory..). The point is # that only the current UEFI bootloader entry is handled. diff --git a/convert2rhel/actions/system_checks/is_loaded_kernel_latest.py b/convert2rhel/actions/system_checks/is_loaded_kernel_latest.py index 1d03868ce..8424f7f63 100644 --- a/convert2rhel/actions/system_checks/is_loaded_kernel_latest.py +++ b/convert2rhel/actions/system_checks/is_loaded_kernel_latest.py @@ -55,6 +55,12 @@ def run(self): # pylint: disable= too-many-return-statements reposdir = get_hardcoded_repofiles_dir() if reposdir and not system_info.has_internet_access: logger.warning("Skipping the check as no internet connection has been detected.") + self.add_message( + level="WARNING", + id="IS_LOADED_KERNEL_LATEST_CHECK_SKIP", + title="Skipping the is loaded kernel latest check", + description="Skipping the check as no internet connection has been detected.", + ) return # If the reposdir variable is not empty, meaning that it detected the @@ -80,6 +86,16 @@ def run(self): # pylint: disable= too-many-return-statements "the %s comparison.\n" "Beware, this could leave your system in a broken state." % package_to_check ) + self.add_message( + level="WARNING", + id="UNSUPPORTED_SKIP_KERNEL_CURRENCY_CHECK_DETECTED", + title="Skipping the kernel currency check", + description=( + "Detected 'CONVERT2RHEL_UNSUPPORTED_SKIP_KERNEL_CURRENCY_CHECK' environment variable, we will skip " + "the %s comparison.\n" + "Beware, this could leave your system in a broken state." % package_to_check + ), + ) return # Look up for available kernel (or kernel-core) packages versions available @@ -96,6 +112,15 @@ def run(self): # pylint: disable= too-many-return-statements "Couldn't fetch the list of the most recent kernels available in " "the repositories. Skipping the loaded kernel check." ) + self.add_message( + level="WARNING", + id="UNABLE_TO_FETCH_RECENT_KERNELS", + title="Unable to fetch recent kernels", + description=( + "Couldn't fetch the list of the most recent kernels available in " + "the repositories. Skipping the loaded kernel check." + ), + ) return packages = [] @@ -120,11 +145,15 @@ def run(self): # pylint: disable= too-many-return-statements self.set_result( level="ERROR", id="KERNEL_CURRENCY_CHECK_FAIL", - message=( - "Could not find any %s from repositories to compare against the loaded kernel.\n" + title="Kernel currency check failed", + description="Please refer to the diagnosis for further information", + diagnosis=( + "Could not find any %s from repositories to compare against the loaded kernel." % package_to_check + ), + remediation=( "Please, check if you have any vendor repositories enabled to proceed with the conversion.\n" "If you wish to ignore this message, set the environment variable " - "'CONVERT2RHEL_UNSUPPORTED_SKIP_KERNEL_CURRENCY_CHECK' to 1." % package_to_check + "'CONVERT2RHEL_UNSUPPORTED_SKIP_KERNEL_CURRENCY_CHECK' to 1." ), ) return @@ -144,7 +173,9 @@ def run(self): # pylint: disable= too-many-return-statements self.set_result( level="ERROR", id="INVALID_KERNEL_PACKAGE", - message=str(exc), + title="Invalid kernel package found", + description="Please refer to the diagnosis for further information", + diagnosis=str(exc), ) return @@ -157,13 +188,17 @@ def run(self): # pylint: disable= too-many-return-statements self.set_result( level="ERROR", id="INVALID_KERNEL_VERSION", - message=( + title="Invalid kernel version detected", + description="The loaded kernel version mismatch the latest one available %s" % repos_message, + diagnosis=( "The version of the loaded kernel is different from the latest version %s.\n" " Latest kernel version available in %s: %s\n" - " Loaded kernel version: %s\n\n" + " Loaded kernel version: %s" % (repos_message, repoid, latest_kernel, loaded_kernel) + ), + remediation=( "To proceed with the conversion, update the kernel version by executing the following step:\n\n" "1. yum install %s-%s -y\n" - "2. reboot" % (repos_message, repoid, latest_kernel, loaded_kernel, package_to_check, latest_kernel) + "2. reboot" % (package_to_check, latest_kernel) ), ) return diff --git a/convert2rhel/actions/system_checks/package_updates.py b/convert2rhel/actions/system_checks/package_updates.py index 76a99ba9e..88f8b849d 100644 --- a/convert2rhel/actions/system_checks/package_updates.py +++ b/convert2rhel/actions/system_checks/package_updates.py @@ -39,27 +39,59 @@ def run(self): "Skipping the check because there are no publicly available %s %d.%d repositories available." % (system_info.name, system_info.version.major, system_info.version.minor) ) + self.add_message( + level="INFO", + id="PACKAGE_UPDATES_CHECK_SKIP_NO_PUBLIC_REPOSITORIES", + title="Skipping the package updates check", + description="Please refer to the diagnosis for further information", + diagnosis=( + "Skipping the check because there are no publicly available %s %d.%d repositories available." + % (system_info.name, system_info.version.major, system_info.version.minor) + ), + ) return reposdir = get_hardcoded_repofiles_dir() if reposdir and not system_info.has_internet_access: logger.warning("Skipping the check as no internet connection has been detected.") + self.add_message( + level="WARNING", + id="PACKAGE_UPDATES_CHECK_SKIP_NO_INTERNET", + title="Skipping the package updates check", + description="Skipping the check as no internet connection has been detected.", + ) return try: packages_to_update = get_total_packages_to_update(reposdir=reposdir) except (utils.UnableToSerialize, pkgmanager.RepoError) as e: - # As both yum and dnf have the same error class (RepoError), to identify any problems when interacting with the - # repositories, we use this to catch exceptions when verifying if there is any packages to update on the system. - # Beware that the `RepoError` exception is based on the `pkgmanager` module and the message sent to the output - # can differ depending if the code is running in RHEL7 (yum) or RHEL8 (dnf). - logger.warning( + # As both yum and dnf have the same error class (RepoError), to + # identify any problems when interacting with the repositories, we + # use this to catch exceptions when verifying if there is any + # packages to update on the system. Beware that the `RepoError` + # exception is based on the `pkgmanager` module and the message + # sent to the output can differ depending if the code is running in + # RHEL7 (yum) or RHEL8 (dnf). + package_up_to_date_error_message = ( "There was an error while checking whether the installed packages are up-to-date. Having an updated system is" " an important prerequisite for a successful conversion. Consider verifyng the system is up to date manually" - " before proceeding with the conversion." + " before proceeding with the conversion. %s" % str(e) + ) + + logger.warning(package_up_to_date_error_message) + self.set_result( + level="SUCCESS", + id="PACKAGE_UP_TO_DATE_CHECK_FAIL", + title="Package up to date check fail", + ) + self.add_message( + level="WARNING", + id="PACKAGE_UP_TO_DATE_CHECK_MESSAGE", + title="Package up to date check fail", + description="Please refer to the diagnosis for further information", + diagnosis=package_up_to_date_error_message, ) - logger.warning(str(e)) return if len(packages_to_update) > 0: @@ -68,12 +100,25 @@ def run(self): if not reposdir else "on repositories defined in the %s folder" % reposdir ) - logger.warning( + package_not_up_to_date_error_message = ( "The system has %s package(s) not updated based %s.\n" "List of packages to update: %s.\n\n" "Not updating the packages may cause the conversion to fail.\n" "Consider updating the packages before proceeding with the conversion." % (len(packages_to_update), repos_message, " ".join(packages_to_update)) ) + logger.warning(package_not_up_to_date_error_message) + self.set_result( + level="SUCCESS", + id="OUT_OF_DATE_PACKAGES", + title="Outdated packages detected", + ) + self.add_message( + level="WARNING", + id="PACKAGE_NOT_UP_TO_DATE_MESSAGE", + title="Outdated packages detected", + description="Please refer to the diagnosis for further information", + diagnosis=package_not_up_to_date_error_message, + ) else: logger.info("System is up-to-date.") diff --git a/convert2rhel/actions/system_checks/readonly_mounts.py b/convert2rhel/actions/system_checks/readonly_mounts.py index 08fac2e13..425a130e4 100644 --- a/convert2rhel/actions/system_checks/readonly_mounts.py +++ b/convert2rhel/actions/system_checks/readonly_mounts.py @@ -54,7 +54,8 @@ def run(self): self.set_result( level="ERROR", id="MNT_DIR_READONLY_MOUNT", - message=( + title="Read-only mount in /mnt directory", + description=( "Stopping conversion due to read-only mount to /mnt directory.\n" "Mount at a subdirectory of /mnt to have /mnt writeable." ), @@ -72,7 +73,8 @@ def run(self): self.set_result( level="ERROR", id="SYS_DIR_READONLY_MOUNT", - message=( + title="Read-only mount in /sys directory", + description=( "Stopping conversion due to read-only mount to /sys directory.\n" "Ensure mount point is writable before executing convert2rhel." ), diff --git a/convert2rhel/actions/system_checks/rhel_compatible_kernel.py b/convert2rhel/actions/system_checks/rhel_compatible_kernel.py index fe3e52554..f38f8dadf 100644 --- a/convert2rhel/actions/system_checks/rhel_compatible_kernel.py +++ b/convert2rhel/actions/system_checks/rhel_compatible_kernel.py @@ -33,6 +33,21 @@ BAD_KERNEL_RELEASE_SUBSTRINGS = ("uek", "rt", "linode") +class KernelIncompatibleError(Exception): + """ + Exception raised when errors are found when rhel incompatible kernels are discovered + """ + + def __init__(self, error_id, template, variables): + self.error_id = error_id + self.template = template + self.variables = variables + + def __str__(self): + # substitute this for the jinga template format + return self.template.format(**self.variables) + + class RhelCompatibleKernel(actions.Action): id = "RHEL_COMPATIBLE_KERNEL" @@ -43,57 +58,66 @@ def run(self): """ super(RhelCompatibleKernel, self).run() logger.task("Prepare: Check kernel compatibility with RHEL") - if any( - ( - _bad_kernel_version(system_info.booted_kernel), - _bad_kernel_package_signature(system_info.booted_kernel), - _bad_kernel_substring(system_info.booted_kernel), - ) - ): - self.set_result( - level="ERROR", - id="BOOTED_KERNEL_INCOMPATIBLE", - message=( - "The booted kernel version is incompatible with the standard RHEL kernel. " - "To proceed with the conversion, boot into a kernel that is available in the {0} {1} base repository" - " by executing the following steps:\n\n" - "1. Ensure that the {0} {1} base repository is enabled\n" - "2. Run: yum install kernel\n" - "3. (optional) Run: grubby --set-default " - '/boot/vmlinuz-`rpm -q --qf "%{{BUILDTIME}}\\t%{{EVR}}.%{{ARCH}}\\n" kernel | sort -nr | head -1 | cut -f2`\n' - "4. Reboot the machine and if step 3 was not applied choose the kernel" - " installed in step 2 manually".format(system_info.name, system_info.version.major) - ), - ) - return - else: - logger.info("The booted kernel %s is compatible with RHEL." % system_info.booted_kernel) + for check_function in (_bad_kernel_version, _bad_kernel_package_signature, _bad_kernel_substring): + try: + check_function(system_info.booted_kernel) + except KernelIncompatibleError as e: + bad_kernel_message = str(e) + logger.warning(bad_kernel_message) + + self.set_result( + level="ERROR", + id=e.error_id, + title="Incompatible booted kernel version", + description="Please refer to the diagnosis for further information", + diagnosis=( + "The booted kernel version is incompatible with the standard RHEL kernel. %s" + % bad_kernel_message + ), + remediation=( + "To proceed with the conversion, boot into a kernel that is available in the {0} {1} base repository" + " by executing the following steps:\n\n" + "1. Ensure that the {0} {1} base repository is enabled\n" + "2. Run: yum install kernel\n" + "3. (optional) Run: grubby --set-default " + '/boot/vmlinuz-`rpm -q --qf "%{{BUILDTIME}}\\t%{{EVR}}.%{{ARCH}}\\n" kernel | sort -nr | head -1 | cut -f2`\n' + "4. Reboot the machine and if step 3 was not applied choose the kernel" + " installed in step 2 manually".format(system_info.name, system_info.version.major) + ), + ) + return + logger.info("The booted kernel %s is compatible with RHEL." % system_info.booted_kernel) def _bad_kernel_version(kernel_release): - """Return True if the booted kernel version does not correspond to the kernel version available in RHEL.""" + """Raises exception if the booted kernel version does not correspond to the kernel version available in RHEL.""" kernel_version = kernel_release.split("-")[0] try: incompatible_version = COMPATIBLE_KERNELS_VERS[system_info.version.major] != kernel_version - if incompatible_version: - logger.warning( - "Booted kernel version '%s' does not correspond to the version " - "'%s' available in RHEL %d" - % ( - kernel_version, - COMPATIBLE_KERNELS_VERS[system_info.version.major], - system_info.version.major, - ) - ) - else: - logger.debug( - "Booted kernel version '%s' corresponds to the version available in RHEL %d" - % (kernel_version, system_info.version.major) - ) - return incompatible_version except KeyError: - logger.debug("Unexpected OS major version. Expected: %r" % COMPATIBLE_KERNELS_VERS.keys()) - return True + raise KernelIncompatibleError( + "UNEXPECTED_VERSION", + "Unexpected OS major version. Expected: {compatible_version}", + dict(compatible_version=COMPATIBLE_KERNELS_VERS.keys()), + ) + + if incompatible_version: + raise KernelIncompatibleError( + "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, + ), + ) + + logger.debug( + "Booted kernel version '%s' corresponds to the version available in RHEL %d" + % (kernel_version, system_info.version.major) + ) + return False def _bad_kernel_package_signature(kernel_release): @@ -106,13 +130,13 @@ def _bad_kernel_package_signature(kernel_release): os_vendor = system_info.name.split()[0] if return_code == 1: - logger.warning( - "The booted kernel %s is not owned by any installed package." - " It needs to be owned by a package signed by %s." % (vmlinuz_path, os_vendor) + raise KernelIncompatibleError( + "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), ) - return True - version, release, arch, name = tuple(kernel_pkg.split("&")) logger.debug("Booted kernel package name: {0}".format(name)) @@ -123,8 +147,11 @@ def _bad_kernel_package_signature(kernel_release): # e.g. Oracle Linux Server -> Oracle or # Oracle Linux Server -> CentOS Linux if bad_signature: - logger.warning("Custom kernel detected. The booted kernel needs to be signed by %s." % os_vendor) - return True + raise KernelIncompatibleError( + "INVALID_KERNEL_PACKAGE_SIGNATURE", + "Custom kernel detected. The booted kernel needs to be signed by {os_vendor}.", + dict(os_vendor=os_vendor), + ) logger.debug("The booted kernel is signed by %s." % os_vendor) return False @@ -134,9 +161,10 @@ def _bad_kernel_substring(kernel_release): """Return True if the booted kernel release contains one of the strings that identify it as non-standard kernel.""" bad_substring = any(bad_substring in kernel_release for bad_substring in BAD_KERNEL_RELEASE_SUBSTRINGS) if bad_substring: - logger.debug( - "The booted kernel '{0}' contains one of the disallowed " - "substrings: {1}".format(kernel_release, BAD_KERNEL_RELEASE_SUBSTRINGS) + raise KernelIncompatibleError( + "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), ) - return True return False diff --git a/convert2rhel/actions/system_checks/tainted_kmods.py b/convert2rhel/actions/system_checks/tainted_kmods.py index d85b1aa25..f21427279 100644 --- a/convert2rhel/actions/system_checks/tainted_kmods.py +++ b/convert2rhel/actions/system_checks/tainted_kmods.py @@ -47,13 +47,17 @@ def run(self): self.set_result( level="ERROR", id="TAINTED_KMODS_DETECTED", - message=( + description="Please refer to the diagnosis for further information", + title="Tainted kernel modules detected", + diagnosis=( "Tainted kernel modules detected:\n {0}\n" "Third-party components are not supported per our " - "software support policy:\n {1}\n" - "Prevent the modules from loading by following {2}" + "software support policy:\n {1}\n".format(module_names, LINK_KMODS_RH_POLICY) + ), + remediation=( + "Prevent the modules from loading by following {0}" " and run convert2rhel again to continue with the conversion.".format( - module_names, LINK_KMODS_RH_POLICY, LINK_PREVENT_KMODS_FROM_LOADING + LINK_PREVENT_KMODS_FROM_LOADING ) ), ) diff --git a/convert2rhel/backup.py b/convert2rhel/backup.py index 513fd0ce8..a21b46391 100644 --- a/convert2rhel/backup.py +++ b/convert2rhel/backup.py @@ -426,6 +426,9 @@ def remove_pkgs( 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 @@ -434,10 +437,17 @@ def remove_pkgs( loggerinst.info("Removing package: %s" % nvra) _, ret_code = run_subprocess(["rpm", "-e", "--nodeps", nvra]) if ret_code != 0: - if critical: - loggerinst.critical("Error: Couldn't remove %s." % nvra) - else: - loggerinst.warning("Couldn't remove %s." % nvra) + pkgs_failed_to_remove.append(nevra) + else: + pkgs_removed.append(nevra) + + if pkgs_failed_to_remove: + if critical: + loggerinst.critical("Error: Couldn't remove %s." % ", ".join(pkgs_failed_to_remove)) + else: + loggerinst.warning("Couldn't remove %s." % ", ".join(pkgs_failed_to_remove)) + + return pkgs_removed def remove_epoch_from_yum_nevra_notation(package_nevra): diff --git a/convert2rhel/logger.py b/convert2rhel/logger.py index 2fe94d8e1..493d2595f 100644 --- a/convert2rhel/logger.py +++ b/convert2rhel/logger.py @@ -171,6 +171,7 @@ def _debug(self, msg, *args, **kwargs): class bcolors: OKGREEN = "\033[92m" + INFO = "\033[94m" WARNING = "\033[93m" FAIL = "\033[91m" ENDC = "\033[0m" diff --git a/convert2rhel/pkghandler.py b/convert2rhel/pkghandler.py index 3760830ff..5876d0617 100644 --- a/convert2rhel/pkghandler.py +++ b/convert2rhel/pkghandler.py @@ -361,10 +361,10 @@ def get_installed_pkgs_w_different_fingerprint(fingerprints, name="*"): @utils.run_as_child_process -def print_pkg_info(pkgs): - """Print package information. +def format_pkg_info(pkgs): + """Format package information. - :param pkgs: List of packages to be printed + :param pkgs: List of packages to be formatted :type pkgs: list[PackageInformation] | list[RPMInstalledPackage] """ package_info = {} @@ -425,10 +425,21 @@ def print_pkg_info(pkgs): ) pkg_table = header + header_underline + pkg_list - loggerinst.info(pkg_table) + return pkg_table +def print_pkg_info(pkgs): + """ + Print the results of format_pkg_info + + :param pkgs: List of packages to be printed + :type pkgs: list[PackageInformation] | list[RPMInstalledPackage] + """ + + loggerinst.info(format_pkg_info(pkgs)) + + def _get_package_repositories(pkgs): """Retrieve repository information from packages. @@ -578,7 +589,7 @@ def list_non_red_hat_pkgs_left(): loggerinst.info("Listing packages not signed by Red Hat") non_red_hat_pkgs = get_installed_pkgs_w_different_fingerprint(system_info.fingerprints_rhel) if non_red_hat_pkgs: - loggerinst.info("The following packages were left unchanged.") + loggerinst.info("The following packages were left unchanged.\n") print_pkg_info(non_red_hat_pkgs) else: loggerinst.info("All packages are now signed by Red Hat.") @@ -596,15 +607,17 @@ def remove_pkgs_unless_from_redhat(pkgs_to_remove, backup=True): loggerinst.info("\nNothing to do.") return - loggerinst.warning("Removing the following %s packages:" % str(len(pkgs_to_remove))) + loggerinst.warning("\nRemoving the following %s packages:\n" % str(len(pkgs_to_remove))) print_pkg_info(pkgs_to_remove) loggerinst.info("\n") - remove_pkgs([get_pkg_nvra(pkg) for pkg in pkgs_to_remove], backup=backup) + pkgs_removed = remove_pkgs([get_pkg_nvra(pkg) for pkg in pkgs_to_remove], backup=backup) loggerinst.debug("Successfully removed %s packages" % str(len(pkgs_to_remove))) + return pkgs_removed + @utils.run_as_child_process -def _get_packages_to_remove(pkgs): +def get_packages_to_remove(pkgs): """ Get packages information that will be removed. @@ -815,7 +828,7 @@ def remove_non_rhel_kernels(): loggerinst.info("Searching for non-RHEL kernels ...") non_rhel_kernels = get_installed_pkgs_w_different_fingerprint(system_info.fingerprints_rhel, "kernel*") if non_rhel_kernels: - loggerinst.info("Removing non-RHEL kernels") + loggerinst.info("Removing non-RHEL kernels\n") print_pkg_info(non_rhel_kernels) remove_pkgs( pkgs_to_remove=[get_pkg_nvra(pkg) for pkg in non_rhel_kernels], diff --git a/convert2rhel/subscription.py b/convert2rhel/subscription.py index 9d32ddce8..45d6716cf 100644 --- a/convert2rhel/subscription.py +++ b/convert2rhel/subscription.py @@ -561,7 +561,6 @@ def remove_original_subscription_manager(): return loggerinst.info("We will now uninstall the following subscription-manager/katello-ca-consumer packages:\n") - pkghandler.print_pkg_info(submgr_pkgs) submgr_pkg_names = [pkg.name for pkg in submgr_pkgs] @@ -729,14 +728,6 @@ def print_avail_subs(subs): loggerinst.info("\n======= Subscription number %d =======\n\n%s" % (index, sub.sub_raw)) -def get_avail_repos(): - """Get list of all the repositories (their IDs) currently available for - the registered system through subscription-manager. - """ - repos_raw, _ = utils.run_subprocess(["subscription-manager", "repos"], print_output=False) - return list(get_repo(repos_raw)) - - def get_repo(repos_raw): """Generator that parses the raw string of available repositores and provides the repository IDs, one at a time. @@ -860,29 +851,6 @@ def rollback(): loggerinst.warning("subscription-manager not installed, skipping") -def check_needed_repos_availability(repo_ids_needed): - """Check whether all the RHEL repositories needed for the system - conversion are available through subscription-manager. - """ - loggerinst.info("Verifying needed RHEL repositories are available ... ") - avail_repos = get_avail_repos() - loggerinst.info("Repositories available through RHSM:\n%s" % "\n".join(avail_repos) + "\n") - - all_repos_avail = True - for repo_id in repo_ids_needed: - if repo_id not in avail_repos: - # TODO: List the packages that would be left untouched - loggerinst.warning( - "Some repositories are not available: %s." - " Some packages may not be replaced with their corresponding" - " RHEL packages when converting. The converted system will end up" - " with a mixture of packages from RHEL and your current distribution." % repo_id - ) - all_repos_avail = False - if all_repos_avail: - loggerinst.info("Needed RHEL repositories are available.") - - def download_rhsm_pkgs(): """Download all the packages necessary for a successful registration to the Red Hat Subscription Management. diff --git a/convert2rhel/unit_tests/__init__.py b/convert2rhel/unit_tests/__init__.py index 1b47fc966..fd0f1f8f1 100644 --- a/convert2rhel/unit_tests/__init__.py +++ b/convert2rhel/unit_tests/__init__.py @@ -419,24 +419,17 @@ def wrapped(*args, **kwargs): class GetInstalledPkgsWFingerprintsMocked(MockFunction): - def prepare_test_pkg_tuples_w_fingerprints(self): - class PkgData: - def __init__(self, pkg_obj, fingerprint): - self.pkg_obj = pkg_obj - self.fingerprint = fingerprint - - obj1 = create_pkg_obj("pkg1") - obj2 = create_pkg_obj("pkg2") - obj3 = create_pkg_obj("gpg-pubkey") - pkgs = [ - PkgData(obj1, "199e2f91fd431d51"), # RHEL - PkgData(obj2, "72f97b74ec551f03"), # OL - PkgData(obj3, "199e2f91fd431d51"), - ] # RHEL - return pkgs + obj1 = create_pkg_information(name="pkg1", fingerprint="199e2f91fd431d51") # RHEL + obj2 = create_pkg_information(name="pkg2", fingerprint="72f97b74ec551f03") # OL + obj3 = create_pkg_information( + name="gpg-pubkey", version="1.0.0", release="1", arch="x86_64", fingerprint="199e2f91fd431d51" # RHEL + ) def __call__(self, *args, **kwargs): - return self.prepare_test_pkg_tuples_w_fingerprints() + return self.get_packages() + + def get_packages(self): + return [self.obj1, self.obj2, self.obj3] #: Used as a sentinel value for assert_action_result() so we only check @@ -444,17 +437,34 @@ def __call__(self, *args, **kwargs): _NO_USER_VALUE = object() -def assert_actions_result(instance, level=_NO_USER_VALUE, id=_NO_USER_VALUE, message=_NO_USER_VALUE): +def assert_actions_result( + instance, + level=_NO_USER_VALUE, + id=_NO_USER_VALUE, + title=_NO_USER_VALUE, + description=_NO_USER_VALUE, + diagnosis=_NO_USER_VALUE, + remediation=_NO_USER_VALUE, +): """Helper function to assert result set by Actions Framework.""" - if level != _NO_USER_VALUE: + if level and level != _NO_USER_VALUE: assert instance.result.level == STATUS_CODE[level] - if id != _NO_USER_VALUE: + if id and id != _NO_USER_VALUE: assert instance.result.id == id - if message != _NO_USER_VALUE: - assert message in instance.result.message + if title and title != _NO_USER_VALUE: + assert title in instance.result.title + + if description and description != _NO_USER_VALUE: + assert description in instance.result.description + + if diagnosis and diagnosis != _NO_USER_VALUE: + assert diagnosis in instance.result.diagnosis + + if remediation and remediation != _NO_USER_VALUE: + assert remediation in instance.result.remediation class EFIBootInfoMocked: diff --git a/convert2rhel/unit_tests/actions/actions_test.py b/convert2rhel/unit_tests/actions/actions_test.py index 7639651ba..74e35fdd7 100644 --- a/convert2rhel/unit_tests/actions/actions_test.py +++ b/convert2rhel/unit_tests/actions/actions_test.py @@ -58,21 +58,27 @@ class TestAction: ( # Set one result field ( - dict(level="SUCCESS"), - dict(level="SUCCESS", id=None, message=None), + dict(level="SUCCESS", id="SUCCESS"), + dict(level="SUCCESS", id="SUCCESS", title=None, description=None, diagnosis=None, remediation=None), ), + # Set all result fields ( - dict(level="SUCCESS", message="Check was skipped because CONVERT2RHEL_SKIP_CHECK was set"), dict( - level="SUCCESS", - id=None, - message="Check was skipped because CONVERT2RHEL_SKIP_CHECK was set", + level="ERROR", + id="ERRORCASE", + title="Problem detected", + description="problem", + diagnosis="detected", + remediation="move on", + ), + dict( + level="ERROR", + id="ERRORCASE", + title="Problem detected", + description="problem", + diagnosis="detected", + remediation="move on", ), - ), - # Set all result fields - ( - dict(level="ERROR", id="ERRORCASE", message="Problem detected"), - dict(level="ERROR", id="ERRORCASE", message="Problem detected"), ), ), ) @@ -83,21 +89,25 @@ def test_set_results_successful(self, set_result_params, expected): action.run() assert action.result.level == STATUS_CODE[expected["level"]] + assert action.result.id == expected["id"] - assert action.result.message == expected["message"] + assert action.result.title == expected["title"] + assert action.result.description == expected["description"] + assert action.result.diagnosis == expected["diagnosis"] + assert action.result.remediation == expected["remediation"] @pytest.mark.parametrize( - ("level",), + ("level", "id"), ( - ("FOOBAR",), - (actions.STATUS_CODE["ERROR"],), + ("FOOBAR", "FOOBAR"), + (actions.STATUS_CODE["ERROR"], "ERROR_ID"), ), ) - def test_set_results_bad_level(self, level): + def test_set_results_bad_level(self, level, id): action = _ActionForTesting(id="TestAction") with pytest.raises(KeyError): - action.set_result(level=level) + action.set_result(level=level, id=id) def test_no_duplicate_ids(self): """Test that each Action has its own unique id.""" @@ -122,17 +132,77 @@ def test_actions_cannot_be_run_twice(self): with pytest.raises(actions.ActionError, match="Action TestAction has already run"): action.run() - def test_add_message(self, monkeypatch): + def test_add_message(self): """Test that add_message formats messages correctly""" action = _ActionForTesting(id="TestAction") - action.add_message(level="WARNING", id="WARNING_ID", message="warning message 1") - action.add_message(level="WARNING", id="WARNING_ID", message="warning message 2") + action.add_message( + level="WARNING", + id="WARNING_ID", + title="warning message 1", + description="action warning", + diagnosis="user warning", + remediation="move on", + ) + action.add_message( + level="WARNING", + id="WARNING_ID", + title="warning message 2", + description="action warning", + diagnosis="user warning", + remediation="move on", + ) + action.add_message( + level="INFO", + id="INFO_ID", + title="info message 1", + description="action info", + diagnosis="user info", + remediation="move on", + ) + action.add_message( + level="INFO", + id="INFO_ID", + title="info message 2", + description="action info", + diagnosis="user info", + remediation="move on", + ) actual_messages = [] for msg in action.messages: actual_messages.append(msg.to_dict()) assert actual_messages == [ - {"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "warning message 1"}, - {"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "warning message 2"}, + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "warning message 1", + "description": "action warning", + "diagnosis": "user warning", + "remediation": "move on", + }, + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "warning message 2", + "description": "action warning", + "diagnosis": "user warning", + "remediation": "move on", + }, + { + "level": STATUS_CODE["INFO"], + "id": "INFO_ID", + "title": "info message 1", + "description": "action info", + "diagnosis": "user info", + "remediation": "move on", + }, + { + "level": STATUS_CODE["INFO"], + "id": "INFO_ID", + "title": "info message 2", + "description": "action info", + "diagnosis": "user info", + "remediation": "move on", + }, ] @@ -658,23 +728,38 @@ class TestRunActions: ( actions.FinishedActions( [ - _ActionForTesting(id="One", messages=[], result=ActionResult(level="SUCCESS")), + _ActionForTesting(id="One", messages=[], result=ActionResult(level="SUCCESS", id="SUCCESS")), ], [], [], ), - {"One": dict(messages=[], result=dict(level=STATUS_CODE["SUCCESS"], id=None, message=""))}, + { + "One": dict( + messages=[], + result=dict( + level=STATUS_CODE["SUCCESS"], + id="SUCCESS", + title="", + description="", + diagnosis="", + remediation="", + ), + ) + }, ), ( actions.FinishedActions( [ _ActionForTesting( - id="One", messages=[], result=ActionResult(level="SUCCESS"), dependencies=("One",) + id="One", + messages=[], + result=ActionResult(level="SUCCESS", id="SUCCESS"), + dependencies=("One",), ), _ActionForTesting( id="Two", messages=[], - result=ActionResult(level="SUCCESS"), + result=ActionResult(level="SUCCESS", id="SUCCESS"), dependencies=( "One", "Two", @@ -685,8 +770,28 @@ class TestRunActions: [], ), { - "One": dict(messages=[], result=dict(level=STATUS_CODE["SUCCESS"], id=None, message="")), - "Two": dict(messages=[], result=dict(level=STATUS_CODE["SUCCESS"], id=None, message="")), + "One": dict( + messages=[], + result=dict( + level=STATUS_CODE["SUCCESS"], + id="SUCCESS", + title="", + description="", + diagnosis="", + remediation="", + ), + ), + "Two": dict( + messages=[], + result=dict( + level=STATUS_CODE["SUCCESS"], + id="SUCCESS", + title="", + description="", + diagnosis="", + remediation="", + ), + ), }, ), # Single Failures @@ -697,14 +802,29 @@ class TestRunActions: _ActionForTesting( id="One", messages=[], - result=ActionResult(level="ERROR", id="SOME_ERROR", message="message"), + result=ActionResult( + level="ERROR", + id="SOME_ERROR", + title="Error", + description="Action error", + diagnosis="User error", + remediation="move on", + ), ), ], [], ), { "One": dict( - messages=[], result=dict(level=STATUS_CODE["ERROR"], id="SOME_ERROR", message="message") + messages=[], + result=dict( + level=STATUS_CODE["ERROR"], + id="SOME_ERROR", + title="Error", + description="Action error", + diagnosis="User error", + remediation="move on", + ), ), }, ), @@ -715,14 +835,29 @@ class TestRunActions: _ActionForTesting( id="One", messages=[], - result=ActionResult(level="OVERRIDABLE", id="SOME_ERROR", message="message"), + result=ActionResult( + level="OVERRIDABLE", + id="SOME_ERROR", + title="Overridable", + description="Action overridable", + diagnosis="User overridable", + remediation="move on", + ), ), ], [], ), { "One": dict( - messages=[], result=dict(level=STATUS_CODE["OVERRIDABLE"], id="SOME_ERROR", message="message") + messages=[], + result=dict( + level=STATUS_CODE["OVERRIDABLE"], + id="SOME_ERROR", + title="Overridable", + description="Action overridable", + diagnosis="User overridable", + remediation="move on", + ), ), }, ), @@ -732,13 +867,30 @@ class TestRunActions: [], [ _ActionForTesting( - id="One", messages=[], result=ActionResult(level="SKIP", id="SOME_ERROR", message="message") + id="One", + messages=[], + result=ActionResult( + level="SKIP", + id="SOME_ERROR", + title="Skip", + description="Action skip", + diagnosis="User skip", + remediation="move on", + ), ), ], ), { "One": dict( - messages=[], result=dict(level=STATUS_CODE["SKIP"], id="SOME_ERROR", message="message") + messages=[], + result=dict( + level=STATUS_CODE["SKIP"], + id="SOME_ERROR", + title="Skip", + description="Action skip", + diagnosis="User skip", + remediation="move on", + ), ), }, ), @@ -746,23 +898,71 @@ class TestRunActions: ( actions.FinishedActions( [ - _ActionForTesting(id="Three", messages=[], result=ActionResult(level="SUCCESS")), + _ActionForTesting(id="Three", messages=[], result=ActionResult(level="SUCCESS", id="SUCCESS")), ], [ _ActionForTesting( - id="One", messages=[], result=ActionResult(level="ERROR", id="ERROR_ID", message="message") + id="One", + messages=[], + result=ActionResult( + level="ERROR", + id="ERROR_ID", + title="Error", + description="Action error", + diagnosis="User error", + remediation="move on", + ), ), ], [ _ActionForTesting( - id="Two", messages=[], result=ActionResult(level="SKIP", id="SKIP_ID", message="message") + id="Two", + messages=[], + result=ActionResult( + level="SKIP", + id="SKIP_ID", + title="Skip", + description="Action skip", + diagnosis="User skip", + remediation="move on", + ), ), ], ), { - "One": dict(messages=[], result=dict(level=STATUS_CODE["ERROR"], id="ERROR_ID", message="message")), - "Two": dict(messages=[], result=dict(level=STATUS_CODE["SKIP"], id="SKIP_ID", message="message")), - "Three": dict(messages=[], result=dict(level=STATUS_CODE["SUCCESS"], id=None, message="")), + "One": dict( + messages=[], + result=dict( + level=STATUS_CODE["ERROR"], + id="ERROR_ID", + title="Error", + description="Action error", + diagnosis="User error", + remediation="move on", + ), + ), + "Two": dict( + messages=[], + result=dict( + level=STATUS_CODE["SKIP"], + id="SKIP_ID", + title="Skip", + description="Action skip", + diagnosis="User skip", + remediation="move on", + ), + ), + "Three": dict( + messages=[], + result=dict( + level=STATUS_CODE["SUCCESS"], + id="SUCCESS", + title="", + description="", + diagnosis="", + remediation="", + ), + ), }, ), ), @@ -785,8 +985,17 @@ def test_run_actions(self, action_results, expected, monkeypatch): [ _ActionForTesting( id="One", - messages=[ActionMessage(level="WARNING", id="WARNING_ID", message="WARNING MESSAGE")], - result=ActionResult(level="SUCCESS"), + messages=[ + ActionMessage( + level="WARNING", + id="WARNING_ID", + title="Warning", + description="Action warning", + diagnosis="User warning", + remediation="move on", + ) + ], + result=ActionResult(level="SUCCESS", id="SUCCESS"), ), ], [], @@ -794,8 +1003,24 @@ def test_run_actions(self, action_results, expected, monkeypatch): ), { "One": dict( - messages=[dict(level=STATUS_CODE["WARNING"], id="WARNING_ID", message="WARNING MESSAGE")], - result=dict(level=STATUS_CODE["SUCCESS"], id=None, message=""), + messages=[ + dict( + level=STATUS_CODE["WARNING"], + id="WARNING_ID", + title="Warning", + description="Action warning", + diagnosis="User warning", + remediation="move on", + ) + ], + result=dict( + level=STATUS_CODE["SUCCESS"], + id="SUCCESS", + title="", + description="", + diagnosis="", + remediation="", + ), ) }, ), @@ -804,14 +1029,32 @@ def test_run_actions(self, action_results, expected, monkeypatch): [ _ActionForTesting( id="One", - messages=[ActionMessage(level="WARNING", id="WARNING_ID", message="WARNING MESSAGE")], - result=ActionResult(level="SUCCESS"), + messages=[ + ActionMessage( + level="WARNING", + id="WARNING_ID", + title="Warning", + description="Action warning", + diagnosis="User warning", + remediation="move on", + ) + ], + result=ActionResult(level="SUCCESS", id="SUCCESS"), dependencies=("One",), ), _ActionForTesting( id="Two", - messages=[ActionMessage(level="WARNING", id="WARNING_ID", message="WARNING MESSAGE")], - result=ActionResult(level="SUCCESS"), + messages=[ + ActionMessage( + level="WARNING", + id="WARNING_ID", + title="Warning", + description="Action warning", + diagnosis="User warning", + remediation="move on", + ) + ], + result=ActionResult(level="SUCCESS", id="SUCCESS"), dependencies=( "One", "Two", @@ -823,12 +1066,44 @@ def test_run_actions(self, action_results, expected, monkeypatch): ), { "One": dict( - messages=[dict(level=STATUS_CODE["WARNING"], id="WARNING_ID", message="WARNING MESSAGE")], - result=dict(level=STATUS_CODE["SUCCESS"], id=None, message=""), + messages=[ + dict( + level=STATUS_CODE["WARNING"], + id="WARNING_ID", + title="Warning", + description="Action warning", + diagnosis="User warning", + remediation="move on", + ) + ], + result=dict( + level=STATUS_CODE["SUCCESS"], + id="SUCCESS", + title="", + description="", + diagnosis="", + remediation="", + ), ), "Two": dict( - messages=[dict(level=STATUS_CODE["WARNING"], id="WARNING_ID", message="WARNING MESSAGE")], - result=dict(level=STATUS_CODE["SUCCESS"], id=None, message=""), + messages=[ + dict( + level=STATUS_CODE["WARNING"], + id="WARNING_ID", + title="Warning", + description="Action warning", + diagnosis="User warning", + remediation="move on", + ) + ], + result=dict( + level=STATUS_CODE["SUCCESS"], + id="SUCCESS", + title="", + description="", + diagnosis="", + remediation="", + ), ), }, ), @@ -839,16 +1114,48 @@ def test_run_actions(self, action_results, expected, monkeypatch): [ _ActionForTesting( id="One", - messages=[ActionMessage(level="WARNING", id="WARNING_ID", message="WARNING MESSAGE")], - result=ActionResult(level="ERROR", id="SOME_ERROR", message="message"), + messages=[ + ActionMessage( + level="WARNING", + id="WARNING_ID", + title="Warning", + description="Action warning", + diagnosis="User warning", + remediation="move on", + ) + ], + result=ActionResult( + level="ERROR", + id="SOME_ERROR", + title="Error", + description="Action error", + diagnosis="User error", + remediation="move on", + ), ), ], [], ), { "One": dict( - messages=[dict(level=STATUS_CODE["WARNING"], id="WARNING_ID", message="WARNING MESSAGE")], - result=dict(level=STATUS_CODE["ERROR"], id="SOME_ERROR", message="message"), + messages=[ + dict( + level=STATUS_CODE["WARNING"], + id="WARNING_ID", + title="Warning", + description="Action warning", + diagnosis="User warning", + remediation="move on", + ) + ], + result=dict( + level=STATUS_CODE["ERROR"], + id="SOME_ERROR", + title="Error", + description="Action error", + diagnosis="User error", + remediation="move on", + ), ), }, ), @@ -858,16 +1165,48 @@ def test_run_actions(self, action_results, expected, monkeypatch): [ _ActionForTesting( id="One", - messages=[ActionMessage(level="WARNING", id="WARNING_ID", message="WARNING MESSAGE")], - result=ActionResult(level="OVERRIDABLE", id="SOME_ERROR", message="message"), + messages=[ + ActionMessage( + level="WARNING", + id="WARNING_ID", + title="Warning", + description="Action warning", + diagnosis="User warning", + remediation="move on", + ) + ], + result=ActionResult( + level="OVERRIDABLE", + id="SOME_ERROR", + title="Overridable", + description="Action overridable", + diagnosis="User overridable", + remediation="move on", + ), ), ], [], ), { "One": dict( - messages=[dict(level=STATUS_CODE["WARNING"], id="WARNING_ID", message="WARNING MESSAGE")], - result=dict(level=STATUS_CODE["OVERRIDABLE"], id="SOME_ERROR", message="message"), + messages=[ + dict( + level=STATUS_CODE["WARNING"], + id="WARNING_ID", + title="Warning", + description="Action warning", + diagnosis="User warning", + remediation="move on", + ) + ], + result=dict( + level=STATUS_CODE["OVERRIDABLE"], + id="SOME_ERROR", + title="Overridable", + description="Action overridable", + diagnosis="User overridable", + remediation="move on", + ), ), }, ), @@ -878,15 +1217,47 @@ def test_run_actions(self, action_results, expected, monkeypatch): [ _ActionForTesting( id="One", - messages=[ActionMessage(level="WARNING", id="WARNING_ID", message="WARNING MESSAGE")], - result=ActionResult(level="SKIP", id="SOME_ERROR", message="message"), + messages=[ + ActionMessage( + level="WARNING", + id="WARNING_ID", + title="Warning", + description="Action warning", + diagnosis="User warning", + remediation="move on", + ) + ], + result=ActionResult( + level="SKIP", + id="SOME_ERROR", + title="Skip", + description="Action skip", + diagnosis="User skip", + remediation="move on", + ), ), ], ), { "One": dict( - messages=[dict(level=STATUS_CODE["WARNING"], id="WARNING_ID", message="WARNING MESSAGE")], - result=dict(level=STATUS_CODE["SKIP"], id="SOME_ERROR", message="message"), + messages=[ + dict( + level=STATUS_CODE["WARNING"], + id="WARNING_ID", + title="Warning", + description="Action warning", + diagnosis="User warning", + remediation="move on", + ) + ], + result=dict( + level=STATUS_CODE["SKIP"], + id="SOME_ERROR", + title="Skip", + description="Action skip", + diagnosis="User skip", + remediation="move on", + ), ), }, ), @@ -896,37 +1267,126 @@ def test_run_actions(self, action_results, expected, monkeypatch): [ _ActionForTesting( id="Three", - messages=[ActionMessage(level="WARNING", id="WARNING_ID", message="WARNING MESSAGE 3")], - result=ActionResult(level="SUCCESS"), + messages=[ + ActionMessage( + level="WARNING", + id="WARNING_ID", + title="Warning", + description="Action warning", + diagnosis="User warning", + remediation="move on", + ) + ], + result=ActionResult(level="SUCCESS", id="SUCCESS"), ), ], [ _ActionForTesting( id="One", - messages=[ActionMessage(level="WARNING", id="WARNING_ID", message="WARNING MESSAGE 1")], - result=ActionResult(level="ERROR", id="ERROR_ID", message="message"), + messages=[ + ActionMessage( + level="WARNING", + id="WARNING_ID", + title="Warning", + description="Action warning", + diagnosis="User warning", + remediation="move on", + ) + ], + result=ActionResult( + level="ERROR", + id="ERROR_ID", + title="Error", + description="Action error", + diagnosis="User error", + remediation="move on", + ), ), ], [ _ActionForTesting( id="Two", - messages=[ActionMessage(level="WARNING", id="WARNING_ID", message="WARNING MESSAGE 2")], - result=ActionResult(level="SKIP", id="SKIP_ID", message="message"), + messages=[ + ActionMessage( + level="WARNING", + id="WARNING_ID", + title="Warning", + description="Action warning", + diagnosis="User warning", + remediation="move on", + ) + ], + result=ActionResult( + level="SKIP", + id="SKIP_ID", + title="Skip", + description="Action skip", + diagnosis="User skip", + remediation="move on", + ), ), ], ), { "One": dict( - messages=[dict(level=STATUS_CODE["WARNING"], id="WARNING_ID", message="WARNING MESSAGE 1")], - result=dict(level=STATUS_CODE["ERROR"], id="ERROR_ID", message="message"), + messages=[ + dict( + level=STATUS_CODE["WARNING"], + id="WARNING_ID", + title="Warning", + description="Action warning", + diagnosis="User warning", + remediation="move on", + ) + ], + result=dict( + level=STATUS_CODE["ERROR"], + id="ERROR_ID", + title="Error", + description="Action error", + diagnosis="User error", + remediation="move on", + ), ), "Two": dict( - messages=[dict(level=STATUS_CODE["WARNING"], id="WARNING_ID", message="WARNING MESSAGE 2")], - result=dict(level=STATUS_CODE["SKIP"], id="SKIP_ID", message="message"), + messages=[ + dict( + level=STATUS_CODE["WARNING"], + id="WARNING_ID", + title="Warning", + description="Action warning", + diagnosis="User warning", + remediation="move on", + ) + ], + result=dict( + level=STATUS_CODE["SKIP"], + id="SKIP_ID", + title="Skip", + description="Action skip", + diagnosis="User skip", + remediation="move on", + ), ), "Three": dict( - messages=[dict(level=STATUS_CODE["WARNING"], id="WARNING_ID", message="WARNING MESSAGE 3")], - result=dict(level=STATUS_CODE["SUCCESS"], id=None, message=""), + messages=[ + dict( + level=STATUS_CODE["WARNING"], + id="WARNING_ID", + title="Warning", + description="Action warning", + diagnosis="User warning", + remediation="move on", + ) + ], + result=dict( + level=STATUS_CODE["SUCCESS"], + id="SUCCESS", + title="", + description="", + diagnosis="", + remediation="", + ), ), }, ), @@ -979,115 +1439,323 @@ def test_find_actions_of_severity(self, severity, expected_ids, key): class TestActionClasses: @pytest.mark.parametrize( - ("id", "level", "message", "expected"), + ("id", "level", "title", "description", "diagnosis", "remediation", "expected"), ( ( - "SUCCESS_ID", "SUCCESS", - "Success message", - dict(id="SUCCESS_ID", level=STATUS_CODE["SUCCESS"], message="Success message"), + "SUCCESS", + None, + None, + None, + None, + dict( + id="SUCCESS", + level=STATUS_CODE["SUCCESS"], + title=None, + description=None, + diagnosis=None, + remediation=None, + ), + ), + ( + "SKIP_ID", + "SKIP", + "Skip message", + "skip description", + "skip diagnosis", + "skip remediation", + dict( + id="SKIP_ID", + level=STATUS_CODE["SKIP"], + title="Skip message", + description="skip description", + diagnosis="skip diagnosis", + remediation="skip remediation", + ), ), - ("SKIP_ID", "SKIP", "Skip message", dict(id="SKIP_ID", level=STATUS_CODE["SKIP"], message="Skip message")), ( "OVERRIDABLE_ID", "OVERRIDABLE", "Overridable message", - dict(id="OVERRIDABLE_ID", level=STATUS_CODE["OVERRIDABLE"], message="Overridable message"), + "overridable description", + "overridable diagnosis", + "overridable remediation", + dict( + id="OVERRIDABLE_ID", + level=STATUS_CODE["OVERRIDABLE"], + title="Overridable message", + description="overridable description", + diagnosis="overridable diagnosis", + remediation="overridable remediation", + ), ), ( "ERROR_ID", "ERROR", "Error message", - dict(id="ERROR_ID", level=STATUS_CODE["ERROR"], message="Error message"), + "error description", + "error diagnosis", + "error remediation", + dict( + id="ERROR_ID", + level=STATUS_CODE["ERROR"], + title="Error message", + description="error description", + diagnosis="error diagnosis", + remediation="error remediation", + ), ), ), ) - def test_action_message_base(self, level, id, message, expected): - action_message_base = ActionMessageBase(level=level, id=id, message=message) + def test_action_message_base(self, level, id, title, description, diagnosis, remediation, expected): + action_message_base = ActionMessageBase( + level=level, id=id, title=title, description=description, diagnosis=diagnosis, remediation=remediation + ) assert action_message_base.to_dict() == expected @pytest.mark.parametrize( - ("id", "level", "message", "expected"), + ("id", "level", "title", "description", "diagnosis", "remediation", "expected"), ( - (None, None, None, "Messages require id, level and message fields"), - ("SUCCESS_ID", None, None, "Messages require id, level and message fields"), - (None, "SUCCESS", None, "Messages require id, level and message fields"), - (None, None, "Success Message", "Messages require id, level and message fields"), - ("SUCCESS_ID", "SUCCESS", "Success message", "Invalid level 'SUCCESS', set for a non-result message"), - ("SKIP_ID", "SKIP", "Skip message", "Invalid level 'SKIP', set for a non-result message"), + (None, None, None, None, None, None, "Messages require id, level, title and description fields"), + ("SUCCESS_ID", None, None, None, None, None, "Messages require id, level, title and description fields"), + (None, "SUCCESS", None, None, None, None, "Messages require id, level, title and description fields"), + ( + None, + None, + "Success Message", + None, + None, + None, + "Messages require id, level, title and description fields", + ), + ( + None, + None, + None, + "Success Description", + None, + None, + "Messages require id, level, title and description fields", + ), + ( + "SUCCESS_ID", + "SUCCESS", + "Success message", + "Description", + None, + None, + "Invalid level 'SUCCESS', set for a non-result message", + ), + ( + "SKIP_ID", + "SKIP", + "Skip message", + "Description", + None, + None, + "Invalid level 'SKIP', set for a non-result message", + ), ( "OVERRIDABLE_ID", "OVERRIDABLE", "Overridable message", + "Description", + None, + None, "Invalid level 'OVERRIDABLE', set for a non-result message", ), - ("ERROR_ID", "ERROR", "Error message", "Invalid level 'ERROR', set for a non-result message"), + ( + "ERROR_ID", + "ERROR", + "Error message", + "Description", + None, + None, + "Invalid level 'ERROR', set for a non-result message", + ), ), ) - def test_action_message_exceptions(self, level, id, message, expected): + def test_action_message_exceptions(self, level, id, title, description, diagnosis, remediation, expected): with pytest.raises(InvalidMessageError, match=expected): - ActionMessage(level=level, id=id, message=message) + ActionMessage( + level=level, id=id, title=title, description=description, diagnosis=diagnosis, remediation=remediation + ) @pytest.mark.parametrize( - ("id", "level", "message", "expected"), + ("id", "level", "title", "description", "diagnosis", "remediation", "expected"), ( ( "WARNING_ID", "WARNING", "Warning message", - dict(id="WARNING_ID", level=STATUS_CODE["WARNING"], message="Warning message"), + "warning description", + "warning diagnosis", + "warning remediation", + dict( + id="WARNING_ID", + level=STATUS_CODE["WARNING"], + title="Warning message", + description="warning description", + diagnosis="warning diagnosis", + remediation="warning remediation", + ), + ), + ( + "INFO_ID", + "INFO", + "Info message", + "info description", + "info diagnosis", + "info remediation", + dict( + id="INFO_ID", + level=STATUS_CODE["INFO"], + title="Info message", + description="info description", + diagnosis="info diagnosis", + remediation="info remediation", + ), ), ), ) - def test_action_message_success(self, level, id, message, expected): - action_message = ActionMessage(level=level, id=id, message=message) + def test_action_message_success(self, level, id, title, description, diagnosis, remediation, expected): + action_message = ActionMessage( + level=level, id=id, title=title, description=description, diagnosis=diagnosis, remediation=remediation + ) assert action_message.to_dict() == expected @pytest.mark.parametrize( - ("id", "level", "message", "expected"), + ("id", "level", "title", "description", "diagnosis", "remediation", "expected"), ( - (None, "ERROR", None, "Non-success results require an id and a message"), - (None, "ERROR", "Error message", "Non-success results require an id"), - ("ERROR_ID", "ERROR", None, "Non-success results require a message"), - (None, "OVERRIDABLE", None, "Non-success results require an id and a message"), - (None, "OVERRIDABLE", "Overiddable message", "Non-success results require an id"), - ("OVERRIDABLE_ID", "OVERRIDABLE", None, "Non-success results require a message"), + ( + None, + "ERROR", + None, + None, + None, + None, + "Results require the id field", + ), + ( + "OVERRIDABLE_ID", + "OVERRIDABLE", + None, + None, + None, + None, + "Non-success results require level, title and description fields", + ), + ( + "ERROR_ID", + "ERROR", + "Title", + None, + None, + None, + "Non-success results require level, title and description fields", + ), + ( + "ERROR_ID", + "ERROR", + None, + "Description", + None, + None, + "Non-success results require level, title and description fields", + ), ( "WARNING_ID", "WARNING", "Warning message", + "Warning description", + "Warning diagnosis", + "Warning remediation", "Invalid level 'WARNING', the level for result must be SKIP or more fatal or SUCCESS.", ), ), ) - def test_action_result_exceptions(self, level, id, message, expected): + def test_action_result_exceptions(self, level, id, title, description, diagnosis, remediation, expected): with pytest.raises(InvalidMessageError, match=expected): - ActionResult(level=level, id=id, message=message) + ActionResult( + level=level, id=id, title=title, description=description, diagnosis=diagnosis, remediation=remediation + ) @pytest.mark.parametrize( - ("id", "level", "message", "expected"), + ("id", "level", "title", "description", "diagnosis", "remediation", "expected"), ( ( "SUCCESS_ID", "SUCCESS", - "Success message", - dict(id="SUCCESS_ID", level=STATUS_CODE["SUCCESS"], message="Success message"), + None, + None, + None, + None, + dict( + id="SUCCESS_ID", + level=STATUS_CODE["SUCCESS"], + title=None, + description=None, + diagnosis=None, + remediation=None, + ), + ), + ( + "SKIP_ID", + "SKIP", + "Skip", + "skip description", + "skip diagnosis", + "skip remediation", + dict( + id="SKIP_ID", + level=STATUS_CODE["SKIP"], + title="Skip", + description="skip description", + diagnosis="skip diagnosis", + remediation="skip remediation", + ), ), - ("SKIP_ID", "SKIP", "Skip message", dict(id="SKIP_ID", level=STATUS_CODE["SKIP"], message="Skip message")), ( "OVERRIDABLE_ID", "OVERRIDABLE", - "Overridable message", - dict(id="OVERRIDABLE_ID", level=STATUS_CODE["OVERRIDABLE"], message="Overridable message"), + "Overridable", + "overridable description", + "overridable diagnosis", + "overridable remediation", + dict( + id="OVERRIDABLE_ID", + level=STATUS_CODE["OVERRIDABLE"], + title="Overridable", + description="overridable description", + diagnosis="overridable diagnosis", + remediation="overridable remediation", + ), ), ( "ERROR_ID", "ERROR", - "Error message", - dict(id="ERROR_ID", level=STATUS_CODE["ERROR"], message="Error message"), + "Error", + "error description", + "error diagnosis", + "error remediation", + dict( + id="ERROR_ID", + level=STATUS_CODE["ERROR"], + title="Error", + description="error description", + diagnosis="error diagnosis", + remediation="error remediation", + ), ), ), ) - def test_action_result_success(self, level, id, message, expected): - action_message = ActionResult(level=level, id=id, message=message) + def test_action_result_success(self, level, id, title, description, diagnosis, remediation, expected): + action_message = ActionResult( + level=level, + id=id, + title=title, + description=description, + diagnosis=diagnosis, + remediation=remediation, + ) assert action_message.to_dict() == expected diff --git a/convert2rhel/unit_tests/actions/data/stage_tests/all_status_actions/test.py b/convert2rhel/unit_tests/actions/data/stage_tests/all_status_actions/test.py index a30a7000b..2c01b2341 100644 --- a/convert2rhel/unit_tests/actions/data/stage_tests/all_status_actions/test.py +++ b/convert2rhel/unit_tests/actions/data/stage_tests/all_status_actions/test.py @@ -7,7 +7,14 @@ class ErrorTest(actions.Action): def run(self): super(ErrorTest, self).run() - self.set_result(level="ERROR", id="ERROR_ID", message="Failed on an error") + self.set_result( + level="ERROR", + id="ERROR_ID", + title="Failed on an error", + description="error", + diagnosis="error", + remediation="error", + ) class OverridableTest(actions.Action): @@ -16,7 +23,14 @@ class OverridableTest(actions.Action): def run(self): super(OverridableTest, self).run() - self.set_result(level="OVERRIDABLE", id="OVERRIDABLE_ID", message="Check failed but user may override") + self.set_result( + level="OVERRIDABLE", + id="OVERRIDABLE_ID", + title="Check failed but user may override", + description="overridable", + diagnosis="overridable", + remediation="overridable", + ) # Skip because one dependency has failed @@ -44,7 +58,14 @@ class WarningTest(actions.Action): def run(self): super(WarningTest, self).run() - self.add_message(level="WARNING", id="WARNING_ID", message="User disabled check") + self.add_message( + level="WARNING", + id="WARNING_ID", + title="User disabled check", + description="warning", + diagnosis="warning", + remediation="warning", + ) class SuccessTest(actions.Action): diff --git a/convert2rhel/unit_tests/actions/data/stage_tests/good_deps_failed_actions/test.py b/convert2rhel/unit_tests/actions/data/stage_tests/good_deps_failed_actions/test.py index 42e22cefd..9645248a0 100644 --- a/convert2rhel/unit_tests/actions/data/stage_tests/good_deps_failed_actions/test.py +++ b/convert2rhel/unit_tests/actions/data/stage_tests/good_deps_failed_actions/test.py @@ -15,4 +15,11 @@ class BTest(actions.Action): def run(self): super(BTest, self).run() - self.set_status(level=actions.STATUS_CODES["ERROR"], id="BTEST_FAILURE", message="failure message") + self.set_status( + level=actions.STATUS_CODES["ERROR"], + id="BTEST_FAILURE", + title="failure title", + description="failure description", + diagnosis="failure diagnosis", + remediation="failure remediation", + ) diff --git a/convert2rhel/unit_tests/actions/pre_ponr_changes/backup_system_test.py b/convert2rhel/unit_tests/actions/pre_ponr_changes/backup_system_test.py index 34730d0a1..c0235da5b 100644 --- a/convert2rhel/unit_tests/actions/pre_ponr_changes/backup_system_test.py +++ b/convert2rhel/unit_tests/actions/pre_ponr_changes/backup_system_test.py @@ -81,7 +81,11 @@ def test_backup_redhat_release_error_system_release_file(self, backup_redhat_rel backup_redhat_release_action.run() unit_tests.assert_actions_result( - backup_redhat_release_action, level="ERROR", id="UNKNOWN_ERROR", message="File not found" + backup_redhat_release_action, + level="ERROR", + id="UNKNOWN_ERROR", + title="An unknown error has occurred", + description="File not found", ) def test_backup_redhat_release_error_os_release_file(self, backup_redhat_release_action, monkeypatch): @@ -90,5 +94,9 @@ def test_backup_redhat_release_error_os_release_file(self, backup_redhat_release backup_redhat_release_action.run() unit_tests.assert_actions_result( - backup_redhat_release_action, level="ERROR", id="UNKNOWN_ERROR", message="File not found" + backup_redhat_release_action, + level="ERROR", + id="UNKNOWN_ERROR", + title="An unknown error has occurred", + description="File not found", ) 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 e2d84c1b0..3e2ba7c32 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,9 +18,10 @@ import pytest import six -from convert2rhel import actions, pkghandler, unit_tests +from convert2rhel import actions, pkghandler, pkgmanager, unit_tests from convert2rhel.actions.pre_ponr_changes import handle_packages from convert2rhel.systeminfo import system_info +from convert2rhel.unit_tests.conftest import centos8 six.add_move(six.MovedModule("mock", "mock", "unittest.mock")) @@ -28,13 +29,13 @@ class PrintPkgInfoMocked(unit_tests.MockFunction): - def __init__(self): + def __init__(self, pkgs): self.called = 0 - self.pkgs = [] + self.pkgs = pkgs def __call__(self, pkgs): self.called += 1 - self.pkgs = pkgs + return self.pkgs @pytest.fixture @@ -51,27 +52,55 @@ def test_list_third_party_packages_no_packages(list_third_party_packages_instanc assert list_third_party_packages_instance.result.level == actions.STATUS_CODE["SUCCESS"] -def test_list_third_party_packages(list_third_party_packages_instance, monkeypatch, caplog): +@centos8 +def test_list_third_party_packages(pretend_os, list_third_party_packages_instance, monkeypatch, caplog): monkeypatch.setattr(pkghandler, "get_third_party_pkgs", unit_tests.GetInstalledPkgsWFingerprintsMocked()) - monkeypatch.setattr(pkghandler, "print_pkg_info", PrintPkgInfoMocked()) - + monkeypatch.setattr(pkghandler, "format_pkg_info", PrintPkgInfoMocked(["shim", "ruby", "pytest"])) + monkeypatch.setattr(system_info, "name", "Centos7") + monkeypatch.setattr(pkgmanager, "TYPE", "dnf") + diagnosis = ( + "Only packages signed by Centos7 are to be" + " replaced. Red Hat support won't be provided" + " for the following third party packages:\npkg1-0:None-None.None, pkg2-0:None-None.None, gpg-pubkey-0:1.0.0-1.x86_64" + ) + expected = set( + ( + actions.ActionMessage( + level="WARNING", + id="THIRD_PARTY_PACKAGE_DETECTED_MESSAGE", + title="Third party packages detected", + description="Third party packages will not be replaced during the conversion.", + diagnosis=diagnosis, + remediation=None, + ), + ) + ) list_third_party_packages_instance.run() + unit_tests.assert_actions_result( + list_third_party_packages_instance, + level="SUCCESS", + id="THIRD_PARTY_PACKAGE_DETECTED", + title="Third party packages detected", + description=None, + diagnosis=None, + remediation=None, + ) - assert len(pkghandler.print_pkg_info.pkgs) == 3 - assert "Only packages signed by" in caplog.records[-1].message - - assert list_third_party_packages_instance.result.level == actions.STATUS_CODE["SUCCESS"] + assert len(pkghandler.format_pkg_info.pkgs) == 3 + assert expected.issuperset(list_third_party_packages_instance.messages) + assert expected.issubset(list_third_party_packages_instance.messages) class CommandCallableObject(unit_tests.MockFunction): - def __init__(self): + def __init__(self, mock_data): self.called = 0 + self.mock_data = mock_data self.command = None def __call__(self, command): self.called += 1 self.command = command - return + return self.mock_data @pytest.fixture @@ -79,22 +108,68 @@ def remove_excluded_packages_instance(): return handle_packages.RemoveExcludedPackages() -def test_remove_excluded_packages(remove_excluded_packages_instance, monkeypatch): +def test_remove_excluded_packages_all_removed(remove_excluded_packages_instance, monkeypatch): + pkgs_to_remove = ["shim", "ruby", "kernel-core"] + pkgs_removed = ["shim", "ruby", "kernel-core"] + expected = set( + ( + actions.ActionMessage( + level="INFO", + id="EXCLUDED_PACKAGES_REMOVED", + title="Excluded packages removed", + description="Excluded packages that have been removed", + diagnosis="The following packages were removed: kernel-core, ruby, shim", + remediation=None, + ), + ) + ) monkeypatch.setattr(system_info, "excluded_pkgs", ["installed_pkg", "not_installed_pkg"]) - monkeypatch.setattr(pkghandler, "_get_packages_to_remove", CommandCallableObject()) - monkeypatch.setattr(pkghandler, "remove_pkgs_unless_from_redhat", CommandCallableObject()) + monkeypatch.setattr(pkghandler, "get_packages_to_remove", CommandCallableObject(pkgs_removed)) + monkeypatch.setattr(pkghandler, "remove_pkgs_unless_from_redhat", CommandCallableObject(pkgs_to_remove)) 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.called == 1 + assert pkghandler.remove_pkgs_unless_from_redhat.called == 1 + assert pkghandler.get_packages_to_remove.command == system_info.excluded_pkgs + assert remove_excluded_packages_instance.result.level == actions.STATUS_CODE["SUCCESS"] + - assert pkghandler._get_packages_to_remove.called == 1 +@centos8 +def test_remove_excluded_packages_not_removed(pretend_os, remove_excluded_packages_instance, monkeypatch): + pkgs_to_remove = unit_tests.GetInstalledPkgsWFingerprintsMocked().get_packages() + 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-0:1.0.0-1.x86_64, pkg1-0:None-None.None, pkg2-0:None-None.None", + remediation=None, + ), + ) + ) + monkeypatch.setattr(system_info, "excluded_pkgs", ["installed_pkg", "not_installed_pkg"]) + monkeypatch.setattr(pkghandler, "get_packages_to_remove", CommandCallableObject(pkgs_to_remove)) + monkeypatch.setattr(pkghandler, "remove_pkgs_unless_from_redhat", CommandCallableObject(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.called == 1 assert pkghandler.remove_pkgs_unless_from_redhat.called == 1 - assert pkghandler._get_packages_to_remove.command == system_info.excluded_pkgs + assert pkghandler.get_packages_to_remove.command == 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", CommandCallableObject()) + monkeypatch.setattr(pkghandler, "get_packages_to_remove", CommandCallableObject(pkgs_removed)) monkeypatch.setattr( pkghandler, "remove_pkgs_unless_from_redhat", mock.Mock(side_effect=SystemExit("Raising SystemExit")) ) @@ -104,8 +179,10 @@ def test_remove_excluded_packages_error(remove_excluded_packages_instance, monke unit_tests.assert_actions_result( remove_excluded_packages_instance, level="ERROR", - id="PACKAGE_REMOVAL_FAILED", - message="Raising SystemExit", + 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", ) @@ -114,16 +191,65 @@ def remove_repository_files_packages_instance(): return handle_packages.RemoveRepositoryFilesPackages() -def test_remove_repository_files_packages(remove_repository_files_packages_instance, monkeypatch): +def test_remove_repository_files_packages_all_removed(remove_repository_files_packages_instance, monkeypatch): + pkgs_to_remove = ["shim", "ruby", "kernel-core"] + pkgs_removed = ["shim", "ruby", "kernel-core"] + expected = set( + ( + actions.ActionMessage( + level="INFO", + id="REPOSITORY_FILE_PACKAGES_REMOVED", + title="Repository file packages removed", + description="Repository file packages that were removed", + diagnosis="The following packages were removed: kernel-core, ruby, shim", + remediation=None, + ), + ) + ) + monkeypatch.setattr(system_info, "repofile_pkgs", ["installed_pkg", "not_installed_pkg"]) + monkeypatch.setattr(pkghandler, "get_packages_to_remove", CommandCallableObject(pkgs_to_remove)) + monkeypatch.setattr(pkghandler, "remove_pkgs_unless_from_redhat", CommandCallableObject(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.called == 1 + assert pkghandler.remove_pkgs_unless_from_redhat.called == 1 + assert pkghandler.get_packages_to_remove.command == 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_to_remove = unit_tests.GetInstalledPkgsWFingerprintsMocked().get_packages() + 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-0:1.0.0-1.x86_64, pkg1-0:None-None.None, pkg2-0:None-None.None", + remediation=None, + ), + ) + ) monkeypatch.setattr(system_info, "repofile_pkgs", ["installed_pkg", "not_installed_pkg"]) - monkeypatch.setattr(pkghandler, "_get_packages_to_remove", CommandCallableObject()) - monkeypatch.setattr(pkghandler, "remove_pkgs_unless_from_redhat", CommandCallableObject()) + monkeypatch.setattr(pkghandler, "get_packages_to_remove", CommandCallableObject(pkgs_to_remove)) + monkeypatch.setattr(pkgmanager, "TYPE", "dnf") + monkeypatch.setattr(pkghandler, "remove_pkgs_unless_from_redhat", CommandCallableObject(pkgs_removed)) remove_repository_files_packages_instance.run() - assert pkghandler._get_packages_to_remove.called == 1 + assert expected.issuperset(remove_repository_files_packages_instance.messages) + assert expected.issubset(remove_repository_files_packages_instance.messages) + assert pkghandler.get_packages_to_remove.called == 1 assert pkghandler.remove_pkgs_unless_from_redhat.called == 1 - assert pkghandler._get_packages_to_remove.command == system_info.repofile_pkgs + assert pkghandler.get_packages_to_remove.command == system_info.repofile_pkgs assert remove_repository_files_packages_instance.result.level == actions.STATUS_CODE["SUCCESS"] @@ -144,6 +270,8 @@ def test_remove_repository_files_packages_error(remove_repository_files_packages unit_tests.assert_actions_result( remove_repository_files_packages_instance, level="ERROR", - id="PACKAGE_REMOVAL_FAILED", - message="Raising SystemExit", + 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", ) diff --git a/convert2rhel/unit_tests/actions/pre_ponr_changes/kernel_modules_test.py b/convert2rhel/unit_tests/actions/pre_ponr_changes/kernel_modules_test.py index 581e0a12b..882d452f9 100644 --- a/convert2rhel/unit_tests/actions/pre_ponr_changes/kernel_modules_test.py +++ b/convert2rhel/unit_tests/actions/pre_ponr_changes/kernel_modules_test.py @@ -21,7 +21,12 @@ import pytest import six +from convert2rhel.actions import STATUS_CODE from convert2rhel.actions.pre_ponr_changes import kernel_modules +from convert2rhel.actions.pre_ponr_changes.kernel_modules import ( + EnsureKernelModulesCompatibility, + RHELKernelModuleNotFound, +) from convert2rhel.systeminfo import system_info from convert2rhel.unit_tests import assert_actions_result, run_subprocess_side_effect from convert2rhel.unit_tests.conftest import centos7, centos8 @@ -138,7 +143,13 @@ def test_ensure_compatibility_of_kmods( if exception: ensure_kernel_modules_compatibility_instance.run() assert_actions_result( - ensure_kernel_modules_compatibility_instance, level="ERROR", id="UNSUPPORTED_KERNEL_MODULES" + ensure_kernel_modules_compatibility_instance, + level="OVERRIDABLE", + id="UNSUPPORTED_KERNEL_MODULES", + title="Unsupported kernel modules", + description="Unsupported kernel modules were found", + diagnosis="The following loaded kernel modules are not available in RHEL:", + remediation="Ensure you have updated the kernel to the latest available version and rebooted the system.", ) else: ensure_kernel_modules_compatibility_instance.run() @@ -147,11 +158,11 @@ def test_ensure_compatibility_of_kmods( if should_be_in_logs: assert should_be_in_logs in caplog.records[-1].message if shouldnt_be_in_logs: - assert shouldnt_be_in_logs not in ensure_kernel_modules_compatibility_instance.result.message + assert shouldnt_be_in_logs not in ensure_kernel_modules_compatibility_instance.result.diagnosis @centos8 -def test_ensure_compatibility_of_kmods_check_env( +def test_ensure_compatibility_of_kmods_check_env_and_message( ensure_kernel_modules_compatibility_instance, monkeypatch, pretend_os, @@ -181,6 +192,13 @@ def test_ensure_compatibility_of_kmods_check_env( " We will continue the conversion with the following kernel modules unavailable in RHEL:.*" ) assert re.match(pattern=should_be_in_logs, string=caplog.records[-1].message, flags=re.MULTILINE | re.DOTALL) + # cannot assert exact action message contents as the kmods arrangement in the message is not static + message = ensure_kernel_modules_compatibility_instance.messages[0] + assert STATUS_CODE["WARNING"] == message.level + assert "ALLOW_UNAVAILABLE_KERNEL_MODULES" == message.id + assert "Skipping the ensure kernel modules compatibility check" == message.title + assert "Detected 'CONVERT2RHEL_ALLOW_UNAVAILABLE_KMODS' environment variable." in message.description + assert "We will continue the conversion with the following kernel modules unavailable in RHEL:" in message.diagnosis @pytest.mark.parametrize( @@ -247,9 +265,12 @@ def test_ensure_compatibility_of_kmods_excluded( ensure_kernel_modules_compatibility_instance.run() assert_actions_result( ensure_kernel_modules_compatibility_instance, - level="ERROR", + level="OVERRIDABLE", id="UNSUPPORTED_KERNEL_MODULES", - message="The following loaded kernel modules are not available in RHEL", + title="Unsupported kernel modules", + description="Unsupported kernel modules were found", + diagnosis="The following loaded kernel modules are not available in RHEL:", + remediation="Ensure you have updated the kernel to the latest available version and rebooted the system.", ) else: ensure_kernel_modules_compatibility_instance.run() @@ -531,3 +552,44 @@ def test_get_unsupported_kmods( result = ensure_kernel_modules_compatibility_instance._get_unsupported_kmods(host_kmods, rhel_supported_kmods) for mod in expected: assert mod in result + + +def test_kernel_modules_rhel_kernel_module_not_found_error(ensure_kernel_modules_compatibility_instance, monkeypatch): + # need to trigger the raise event + monkeypatch.setattr( + EnsureKernelModulesCompatibility, + "_get_rhel_supported_kmods", + mock.Mock( + side_effect=RHELKernelModuleNotFound( + "No packages containing kernel modules available in the enabled repositories" + ) + ), + ) + ensure_kernel_modules_compatibility_instance.run() + print(ensure_kernel_modules_compatibility_instance.result) + assert_actions_result( + ensure_kernel_modules_compatibility_instance, + level="ERROR", + id="NO_RHEL_KERNEL_MODULES_FOUND", + title="No RHEL kernel modules were found", + description="This check was unable to find any kernel modules in the packages in the enabled yum repositories.", + diagnosis="No packages containing kernel modules available in the enabled repositories", + remediation="Adding additional repositories to those mentioned in the diagnosis may solve this issue.", + ) + + +def test_kernel_modules_value_error(ensure_kernel_modules_compatibility_instance, monkeypatch): + # need to trigger the raise event + monkeypatch.setattr( + EnsureKernelModulesCompatibility, "_get_loaded_kmods", mock.Mock(side_effect=ValueError("Value error")) + ) + ensure_kernel_modules_compatibility_instance.run() + print(ensure_kernel_modules_compatibility_instance.result) + assert_actions_result( + ensure_kernel_modules_compatibility_instance, + level="ERROR", + id="CANNOT_COMPARE_PACKAGE_VERSIONS", + title="Error while comparing packages", + description="There was an error while detecting the kernel package which corresponds to the kernel modules present on the system.", + diagnosis="Package comparison failed: Value error", + ) diff --git a/convert2rhel/unit_tests/actions/pre_ponr_changes/special_cases_test.py b/convert2rhel/unit_tests/actions/pre_ponr_changes/special_cases_test.py index d7c5dd827..4cae8a4c2 100644 --- a/convert2rhel/unit_tests/actions/pre_ponr_changes/special_cases_test.py +++ b/convert2rhel/unit_tests/actions/pre_ponr_changes/special_cases_test.py @@ -109,3 +109,47 @@ def test_remove_iwlax2xx_firmware_not_ol8(pretend_os, caplog): assert "Relevant to Oracle Linux 8 only. Skipping." in caplog.records[-1].message assert instance.result.level == actions.STATUS_CODE["SUCCESS"] + + +@oracle8 +@pytest.mark.parametrize( + ( + "is_iwl7260_installed", + "is_iwlax2xx_installed", + "subprocess_output", + "subprocess_call_count", + ), + ((True, True, ("output", 1), 1),), +) +def test_remove_iwlax2xx_firmware_message( + pretend_os, is_iwl7260_installed, is_iwlax2xx_installed, subprocess_output, subprocess_call_count, monkeypatch +): + run_subprocess_mock = mock.Mock( + side_effect=run_subprocess_side_effect( + (("rpm", "-e", "--nodeps", "iwlax2xx-firmware"), subprocess_output), + ) + ) + is_rpm_installed_mock = mock.Mock(side_effect=[is_iwl7260_installed, is_iwlax2xx_installed]) + monkeypatch.setattr( + special_cases, + "run_subprocess", + value=run_subprocess_mock, + ) + monkeypatch.setattr(special_cases.system_info, "is_rpm_installed", value=is_rpm_installed_mock) + expected = set( + ( + actions.ActionMessage( + level="WARNING", + id="IWLAX2XX_FIRMWARE_REMOVAL_FAILED", + title="Package removal failed", + description="Unable to remove the package iwlax2xx-firmware.", + diagnosis=None, + remediation=None, + ), + ) + ) + + instance = special_cases.RemoveIwlax2xxFirmware() + instance.run() + assert expected.issuperset(instance.messages) + assert expected.issubset(instance.messages) 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 05fb155a6..a04c74419 100644 --- a/convert2rhel/unit_tests/actions/pre_ponr_changes/subscription_test.py +++ b/convert2rhel/unit_tests/actions/pre_ponr_changes/subscription_test.py @@ -18,7 +18,7 @@ import pytest import six -from convert2rhel import cert, pkghandler, repo, subscription, toolopts, unit_tests +from convert2rhel import actions, cert, pkghandler, repo, subscription, toolopts, unit_tests from convert2rhel.actions import STATUS_CODE from convert2rhel.actions.pre_ponr_changes.subscription import PreSubscription, SubscribeSystem @@ -51,11 +51,24 @@ def test_pre_subscription_dependency_order(pre_subscription_instance): def test_pre_subscription_no_rhsm_option_detected(pre_subscription_instance, monkeypatch, caplog): monkeypatch.setattr(toolopts.tool_opts, "no_rhsm", True) - + expected = set( + ( + actions.ActionMessage( + level="WARNING", + id="PRE_SUBSCRIPTION_CHECK_SKIP", + title="Pre-subscription check skip", + description="Detected --no-rhsm option. Skipping.", + diagnosis=None, + remediation=None, + ), + ) + ) pre_subscription_instance.run() assert "Detected --no-rhsm option. Skipping" in caplog.records[-1].message assert pre_subscription_instance.result.level == STATUS_CODE["SUCCESS"] + assert expected.issuperset(pre_subscription_instance.messages) + assert expected.issubset(pre_subscription_instance.messages) def test_pre_subscription_run(pre_subscription_instance, monkeypatch): @@ -77,8 +90,10 @@ def test_pre_subscription_run(pre_subscription_instance, monkeypatch): @pytest.mark.parametrize( ("exception", "expected_level"), ( - (SystemExit("Exiting..."), ("ERROR", "UNKNOWN_ERROR", "Exiting...")), - (subscription.UnregisterError, ("ERROR", "UNABLE_TO_REGISTER", "Failed to unregister the system:")), + ( + SystemExit("Exiting..."), + ("ERROR", "UNKNOWN_ERROR", "Unknown error", "The cause of this error is unknown", "Exiting..."), + ), ), ) def test_pre_subscription_exceptions(exception, expected_level, pre_subscription_instance, monkeypatch): @@ -88,9 +103,43 @@ def test_pre_subscription_exceptions(exception, expected_level, pre_subscription monkeypatch.setattr(pkghandler, "install_gpg_keys", mock.Mock(side_effect=exception)) pre_subscription_instance.run() + level, id, title, description, diagnosis = expected_level + unit_tests.assert_actions_result(pre_subscription_instance, level=level, id=id, description=description) - level, id, message = expected_level - unit_tests.assert_actions_result(pre_subscription_instance, level=level, id=id, message=message) + +@pytest.mark.parametrize( + ("exception", "expected_level"), + ( + ( + subscription.UnregisterError, + ( + "ERROR", + "UNABLE_TO_REGISTER", + "System unregistration failure", + "The system is already registered with subscription-manager", + "Failed to unregister the system:", + "You may want to unregister the system manually", + ), + ), + ), +) +def test_pre_subscription_exceptions_with_remediation( + exception, expected_level, pre_subscription_instance, monkeypatch +): + # In the actual code, the exceptions can happen at different stages, but + # since it is a unit test, it doesn't matter what function will raise the + # exception we want. + monkeypatch.setattr(pkghandler, "install_gpg_keys", mock.Mock(side_effect=exception)) + + pre_subscription_instance.run() + level, id, title, description, diagnosis, remediation = expected_level + unit_tests.assert_actions_result( + pre_subscription_instance, + level=level, + id=id, + description=description, + diagnosis=diagnosis, + ) @pytest.fixture @@ -109,17 +158,28 @@ def test_subscribe_system_dependency_order(subscribe_system_instance): def test_subscribe_system_no_rhsm_option_detected(subscribe_system_instance, monkeypatch, caplog): monkeypatch.setattr(toolopts.tool_opts, "no_rhsm", True) - + expected = set( + ( + actions.ActionMessage( + level="WARNING", + id="SUBSCRIPTION_CHECK_SKIP", + title="Subscription check skip", + description="Detected --no-rhsm option. Skipping.", + diagnosis=None, + remediation=None, + ), + ) + ) subscribe_system_instance.run() - assert "Detected --no-rhsm option. Skipping" in caplog.records[-1].message assert subscribe_system_instance.result.level == STATUS_CODE["SUCCESS"] + assert expected.issuperset(subscribe_system_instance.messages) + assert expected.issubset(subscribe_system_instance.messages) def test_subscribe_system_run(subscribe_system_instance, monkeypatch): monkeypatch.setattr(subscription, "subscribe_system", mock.Mock()) monkeypatch.setattr(repo, "get_rhel_repoids", mock.Mock()) - monkeypatch.setattr(subscription, "check_needed_repos_availability", mock.Mock()) monkeypatch.setattr(subscription, "disable_repos", mock.Mock()) monkeypatch.setattr(subscription, "enable_repos", mock.Mock()) @@ -128,7 +188,6 @@ def test_subscribe_system_run(subscribe_system_instance, monkeypatch): assert subscribe_system_instance.result.level == STATUS_CODE["SUCCESS"] assert subscription.subscribe_system.call_count == 1 assert repo.get_rhel_repoids.call_count == 1 - assert subscription.check_needed_repos_availability.call_count == 1 assert subscription.disable_repos.call_count == 1 assert subscription.enable_repos.call_count == 1 @@ -136,16 +195,30 @@ def test_subscribe_system_run(subscribe_system_instance, monkeypatch): @pytest.mark.parametrize( ("exception", "expected_level"), ( - (IOError("/usr/bin/t"), ("ERROR", "MISSING_SUBSCRIPTION_MANAGER_BINARY", "Failed to execute command:")), - (SystemExit("Exiting..."), ("ERROR", "UNKNOWN_ERROR", "Exiting...")), + ( + SystemExit("Exiting..."), + ("ERROR", "UNKNOWN_ERROR", "Unknown error", "The cause of this error is unknown", "Exiting..."), + ), ( ValueError, ( "ERROR", "MISSING_REGISTRATION_COMBINATION", + "Missing registration combination", + "There are missing registration combinations", "One or more combinations were missing for subscription-manager parameters:", ), ), + ( + IOError("/usr/bin/t"), + ( + "ERROR", + "MISSING_SUBSCRIPTION_MANAGER_BINARY", + "Missing subscription-manager binary", + "There is a missing subscription-manager binary", + "Failed to execute command:", + ), + ), ), ) def test_subscribe_system_exceptions(exception, expected_level, subscribe_system_instance, monkeypatch): @@ -156,5 +229,7 @@ def test_subscribe_system_exceptions(exception, expected_level, subscribe_system subscribe_system_instance.run() - level, id, message = expected_level - unit_tests.assert_actions_result(subscribe_system_instance, level=level, id=id, message=message) + level, id, title, description, diagnosis = expected_level + unit_tests.assert_actions_result( + subscribe_system_instance, level=level, id=id, title=title, description=description, diagnosis=diagnosis + ) 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 1c8539b33..e7f1cd039 100644 --- a/convert2rhel/unit_tests/actions/pre_ponr_changes/transaction_test.py +++ b/convert2rhel/unit_tests/actions/pre_ponr_changes/transaction_test.py @@ -77,5 +77,10 @@ def test_validate_package_manager_transaction_unknown_error( validate_package_manager_transaction.run() unit_tests.assert_actions_result( - validate_package_manager_transaction, level="ERROR", id="UNKNOWN_ERROR", message="Exiting..." + validate_package_manager_transaction, + level="ERROR", + id="UNKNOWN_ERROR", + title="Unknown", + description="The cause of this error is unknown, please look at the diagnosis for more information.", + diagnosis="Exiting...", ) diff --git a/convert2rhel/unit_tests/actions/report_test.py b/convert2rhel/unit_tests/actions/report_test.py index dc6256912..3d79a00e3 100644 --- a/convert2rhel/unit_tests/actions/report_test.py +++ b/convert2rhel/unit_tests/actions/report_test.py @@ -26,7 +26,12 @@ #: _LONG_MESSAGE since we do line wrapping -_LONG_MESSAGE = "Will Robinson! Will Robinson! Danger Will Robinson...! Please report directly to your parents in the spaceship immediately. Danger! Danger! Danger!" +_LONG_MESSAGE = { + "title": "Will Robinson! Will Robinson!", + "description": " Danger Will Robinson...!", + "diagnosis": " Danger! Danger! Danger!", + "remediation": " Please report directly to your parents in the spaceship immediately.", +} @pytest.mark.parametrize( @@ -75,45 +80,71 @@ def test_summary_as_json(results, expected, tmpdir): ( { "PreSubscription": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE"}], - result={"level": STATUS_CODE["SUCCESS"], "id": None, "message": "All good!"}, - ) - }, - True, - ["(WARNING) PreSubscription::WARNING_ID - WARNING MESSAGE", "(SUCCESS) PreSubscription - All good!"], - ), - ( - { - "PreSubscription": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE"}], - result={"level": STATUS_CODE["SUCCESS"], "id": None, "message": None}, + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], + result={ + "level": STATUS_CODE["SUCCESS"], + "id": "SUCCESS", + "title": "", + "description": "", + "diagnosis": "", + "remediation": "", + }, ) }, True, [ - "(WARNING) PreSubscription::WARNING_ID - WARNING MESSAGE", - "(SUCCESS) PreSubscription - [No further information given]", + "(WARNING) PreSubscription::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on", + "(SUCCESS) PreSubscription::SUCCESS - [No further information given]", ], ), ( { "PreSubscription": dict( - messages=[], result={"level": STATUS_CODE["SUCCESS"], "id": None, "message": "All good!"} + messages=[], + result={ + "level": STATUS_CODE["SUCCESS"], + "id": "SUCCESS", + "title": "", + "description": "", + "diagnosis": "", + "remediation": "", + }, ), "PreSubscription2": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE"}], + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], result={ "level": STATUS_CODE["SKIP"], "id": "SKIPPED", - "message": "SKIP MESSAGE", + "title": "Skip", + "description": "Action skip", + "diagnosis": "User skip", + "remediation": "move on", }, ), }, True, [ - "(SUCCESS) PreSubscription - All good!", - "(WARNING) PreSubscription2::WARNING_ID - WARNING MESSAGE", - "(SKIP) PreSubscription2::SKIPPED - SKIP MESSAGE", + "(SUCCESS) PreSubscription::SUCCESS - [No further information given]", + "(WARNING) PreSubscription2::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on", + "(SKIP) PreSubscription2::SKIPPED - Skip\n Description: Action skip\n Diagnosis: User skip\n Remediation: move on", ], ), # Test that messages that are below WARNING will not appear in @@ -121,7 +152,15 @@ def test_summary_as_json(results, expected, tmpdir): ( { "PreSubscription": dict( - messages=[], result={"level": STATUS_CODE["SUCCESS"], "id": None, "message": None} + messages=[], + result={ + "level": STATUS_CODE["SUCCESS"], + "id": "SUCCESS", + "title": "", + "description": "", + "diagnosis": "", + "remediation": "", + }, ) }, False, @@ -130,100 +169,226 @@ def test_summary_as_json(results, expected, tmpdir): ( { "PreSubscription": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE"}], - result={"level": STATUS_CODE["SUCCESS"], "id": None, "message": None}, + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], + result={ + "level": STATUS_CODE["SUCCESS"], + "id": "SUCCESS", + "title": "", + "description": "", + "diagnosis": "", + "remediation": "", + }, ) }, False, - ["(WARNING) PreSubscription::WARNING_ID - WARNING MESSAGE"], + [ + "(WARNING) PreSubscription::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on" + ], ), ( { "PreSubscription": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE 1"}], - result={"level": STATUS_CODE["SUCCESS"], "id": None, "message": None}, + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], + result={ + "level": STATUS_CODE["SUCCESS"], + "id": "SUCCESS", + "title": "", + "description": "", + "diagnosis": "", + "remediation": "", + }, ), "PreSubscription2": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE 2"}], + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], result={ "level": STATUS_CODE["SKIP"], "id": "SKIPPED", - "message": "SKIP MESSAGE", + "title": "Skip", + "description": "Action skip", + "diagnosis": "User skip", + "remediation": "move on", }, ), }, False, [ - "(SKIP) PreSubscription2::SKIPPED - SKIP MESSAGE", - "(WARNING) PreSubscription::WARNING_ID - WARNING MESSAGE 1", - "(WARNING) PreSubscription2::WARNING_ID - WARNING MESSAGE 2", + "(SKIP) PreSubscription2::SKIPPED - Skip\n Description: Action skip\n Diagnosis: User skip\n Remediation: move on", + "(WARNING) PreSubscription::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on", + "(WARNING) PreSubscription2::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on", ], ), # Test all messages are displayed, SKIP and higher ( { "PreSubscription1": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE 1"}], + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], result={ "level": STATUS_CODE["SKIP"], "id": "SKIPPED", - "message": "SKIP MESSAGE", + "title": "Skip", + "description": "Action skip", + "diagnosis": "User skip", + "remediation": "move on", }, ), "PreSubscription2": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE 2"}], + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], result={ "level": STATUS_CODE["OVERRIDABLE"], "id": "OVERRIDABLE_ID", - "message": "OVERRIDABLE MESSAGE", + "title": "Overridable", + "description": "Action overridable", + "diagnosis": "User overridable", + "remediation": "move on", }, ), }, False, [ - "(OVERRIDABLE) PreSubscription2::OVERRIDABLE_ID - OVERRIDABLE MESSAGE", - "(SKIP) PreSubscription1::SKIPPED - SKIP MESSAGE", - "(WARNING) PreSubscription1::WARNING_ID - WARNING MESSAGE 1", - "(WARNING) PreSubscription2::WARNING_ID - WARNING MESSAGE 2", + "(OVERRIDABLE) PreSubscription2::OVERRIDABLE_ID - Overridable\n Description: Action overridable\n Diagnosis: User overridable\n Remediation: move on", + "(SKIP) PreSubscription1::SKIPPED - Skip\n Description: Action skip\n Diagnosis: User skip\n Remediation: move on", + "(WARNING) PreSubscription1::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on", + "(WARNING) PreSubscription2::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on", ], ), ( { "SkipAction": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE 4"}], - result={"level": STATUS_CODE["SKIP"], "id": "SKIP", "message": "SKIP MESSAGE"}, + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], + result={ + "level": STATUS_CODE["SKIP"], + "id": "SKIP", + "title": "Skip", + "description": "Action skip", + "diagnosis": "User skip", + "remediation": "move on", + }, ), "OverridableAction": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE 3"}], + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], result={ "level": STATUS_CODE["OVERRIDABLE"], "id": "OVERRIDABLE", - "message": "OVERRIDABLE MESSAGE", + "title": "Overridable", + "description": "Action overridable", + "diagnosis": "User overridable", + "remediation": "move on", }, ), "ErrorAction": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE 2"}], - result={"level": STATUS_CODE["ERROR"], "id": "ERROR", "message": "ERROR MESSAGE"}, + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], + result={ + "level": STATUS_CODE["ERROR"], + "id": "ERROR", + "title": "Error", + "description": "Action error", + "diagnosis": "User error", + "remediation": "move on", + }, ), "TestAction": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE 1"}], + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], result={ "level": STATUS_CODE["ERROR"], "id": "SECONDERROR", - "message": "Test that two of the same level works", + "title": "Error", + "description": "Action error", + "diagnosis": "User error", + "remediation": "move on", }, ), }, False, [ - "(ERROR) ErrorAction::ERROR - ERROR MESSAGE", - "(ERROR) TestAction::SECONDERROR - Test that two of the same level works", - "(OVERRIDABLE) OverridableAction::OVERRIDABLE - OVERRIDABLE MESSAGE", - "(SKIP) SkipAction::SKIP - SKIP MESSAGE", - "(WARNING) SkipAction::WARNING_ID - WARNING MESSAGE 4", - "(WARNING) OverridableAction::WARNING_ID - WARNING MESSAGE 3", - "(WARNING) ErrorAction::WARNING_ID - WARNING MESSAGE 2", - "(WARNING) TestAction::WARNING_ID - WARNING MESSAGE 1", + "(ERROR) ErrorAction::ERROR - Error\n Description: Action error\n Diagnosis: User error\n Remediation: move on", + "(ERROR) TestAction::SECONDERROR - Error\n Description: Action error\n Diagnosis: User error\n Remediation: move on", + "(OVERRIDABLE) OverridableAction::OVERRIDABLE - Overridable\n Description: Action overridable\n Diagnosis: User overridable\n Remediation: move on", + "(SKIP) SkipAction::SKIP - Skip\n Description: Action skip\n Diagnosis: User skip\n Remediation: move on", + "(WARNING) SkipAction::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on", + "(WARNING) OverridableAction::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on", + "(WARNING) ErrorAction::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on", + "(WARNING) TestAction::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on", ], ), ), @@ -232,20 +397,32 @@ def test_summary(results, expected_results, include_all_reports, caplog): report.summary(results, include_all_reports, with_colors=False) for expected in expected_results: - assert expected in caplog.records[-1].message.splitlines() + assert expected in caplog.records[-1].message -def test_results_summary_with_long_message(caplog): +@pytest.mark.parametrize( + ("long_message"), + ( + (_LONG_MESSAGE), + ( + { + "title": "Will Robinson! Will Robinson!", + "description": " Danger Will Robinson...!" * 8, + "diagnosis": " Danger!" * 15, + "remediation": " Please report directly to your parents in the spaceship immediately." * 2, + } + ), + ), +) +def test_results_summary_with_long_message(long_message, caplog): """Test a long message because we word wrap those.""" + result = {"level": STATUS_CODE["ERROR"], "id": "ERROR"} + result.update(long_message) report.summary( { "ErrorAction": dict( messages=[], - result={ - "level": STATUS_CODE["ERROR"], - "id": "ERROR", - "message": _LONG_MESSAGE, - }, + result=result, ) }, with_colors=False, @@ -253,17 +430,49 @@ def test_results_summary_with_long_message(caplog): # Word wrapping might break on any spaces so we need to substitute # a pattern for those - pattern = _LONG_MESSAGE.replace(" ", "[ \t\n]+") + pattern = long_message["title"].replace(" ", "[ \t\n]+") + assert re.search(pattern, caplog.records[-1].message) + + pattern = long_message["description"].replace(" ", "[ \t\n]+") assert re.search(pattern, caplog.records[-1].message) + pattern = long_message["diagnosis"].replace(" ", "[ \t\n]+") + assert re.search(pattern, caplog.records[-1].message) -def test_messages_summary_with_long_message(caplog): + pattern = long_message["remediation"].replace(" ", "[ \t\n]+") + assert re.search(pattern, caplog.records[-1].message) + + +@pytest.mark.parametrize( + ("long_message"), + ( + (_LONG_MESSAGE), + ( + { + "title": "Will Robinson! Will Robinson!", + "description": " Danger Will Robinson...!" * 8, + "diagnosis": " Danger!" * 15, + "remediation": " Please report directly to your parents in the spaceship immediately." * 2, + } + ), + ), +) +def test_messages_summary_with_long_message(long_message, caplog): """Test a long message because we word wrap those.""" + messages = {"level": STATUS_CODE["WARNING"], "id": "WARNING_ID"} + messages.update(long_message) report.summary( { "ErrorAction": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": _LONG_MESSAGE}], - result={"level": STATUS_CODE["SUCCESS"], "id": "", "message": ""}, + messages=[messages], + result={ + "level": STATUS_CODE["SUCCESS"], + "id": "", + "title": "", + "description": "", + "diagnosis": "", + "remediation": "", + }, ) }, with_colors=False, @@ -271,7 +480,16 @@ def test_messages_summary_with_long_message(caplog): # Word wrapping might break on any spaces so we need to substitute # a pattern for those - pattern = _LONG_MESSAGE.replace(" ", "[ \t\n]+") + pattern = long_message["title"].replace(" ", "[ \t\n]+") + assert re.search(pattern, caplog.records[-1].message) + + pattern = long_message["description"].replace(" ", "[ \t\n]+") + assert re.search(pattern, caplog.records[-1].message) + + pattern = long_message["diagnosis"].replace(" ", "[ \t\n]+") + assert re.search(pattern, caplog.records[-1].message) + + pattern = long_message["remediation"].replace(" ", "[ \t\n]+") assert re.search(pattern, caplog.records[-1].message) @@ -283,47 +501,74 @@ def test_messages_summary_with_long_message(caplog): { "PreSubscription2": dict( messages=[], - result={"level": STATUS_CODE["SKIP"], "id": "SKIPPED", "message": "SKIP MESSAGE"}, + result={ + "level": STATUS_CODE["SKIP"], + "id": "SKIPPED", + "title": "Skipped", + "description": "Action skip", + "diagnosis": "User skip", + "remediation": "move on", + }, ), "PreSubscription1": dict( messages=[], result={ "level": STATUS_CODE["OVERRIDABLE"], "id": "SOME_OVERRIDABLE", - "message": "OVERRIDABLE MESSAGE", + "title": "Overridable", + "description": "Action override", + "diagnosis": "User override", + "remediation": "move on", }, ), }, False, [ - r"\(OVERRIDABLE\) PreSubscription1::SOME_OVERRIDABLE - OVERRIDABLE MESSAGE", - r"\(SKIP\) PreSubscription2::SKIPPED - SKIP MESSAGE", + r"\(OVERRIDABLE\) PreSubscription1::SOME_OVERRIDABLE - Overridable\n Description: Action override\n Diagnosis: User override\n Remediation: move on", + r"\(SKIP\) PreSubscription2::SKIPPED - Skipped\n Description: Action skip\n Diagnosis: User skip\n Remediation: move on", ], ), ( { "SkipAction": dict( messages=[], - result={"level": STATUS_CODE["SKIP"], "id": "SKIP", "message": "SKIP MESSAGE"}, + result={ + "level": STATUS_CODE["SKIP"], + "id": "SKIP", + "title": "Skip", + "description": "Action skip", + "diagnosis": "User skip", + "remediation": "move on", + }, ), "OverridableAction": dict( messages=[], result={ "level": STATUS_CODE["OVERRIDABLE"], "id": "OVERRIDABLE", - "message": "OVERRIDABLE MESSAGE", + "title": "Overridable", + "description": "Action override", + "diagnosis": "User override", + "remediation": "move on", }, ), "ErrorAction": dict( messages=[], - result={"level": STATUS_CODE["ERROR"], "id": "ERROR", "message": "ERROR MESSAGE"}, + result={ + "level": STATUS_CODE["ERROR"], + "id": "ERROR", + "title": "Error", + "description": "Action error", + "diagnosis": "User error", + "remediation": "move on", + }, ), }, False, [ - r"\(ERROR\) ErrorAction::ERROR - ERROR MESSAGE", - r"\(OVERRIDABLE\) OverridableAction::OVERRIDABLE - OVERRIDABLE MESSAGE", - r"\(SKIP\) SkipAction::SKIP - SKIP MESSAGE", + r"\(ERROR\) ErrorAction::ERROR - Error\n Description: Action error\n Diagnosis: User error\n Remediation: move on", + r"\(OVERRIDABLE\) OverridableAction::OVERRIDABLE - Overridable\n Description: Action override\n Diagnosis: User override\n Remediation: move on", + r"\(SKIP\) SkipAction::SKIP - Skip\n Description: Action skip\n Diagnosis: User skip\n Remediation: move on", ], ), # Message order with `include_all_reports` set to True. @@ -331,31 +576,55 @@ def test_messages_summary_with_long_message(caplog): { "PreSubscription": dict( messages=[], - result={"level": STATUS_CODE["SUCCESS"], "id": None, "message": "All good!"}, + result={ + "level": STATUS_CODE["SUCCESS"], + "id": "SUCCESS", + "title": "", + "description": "", + "diagnosis": "", + "remediation": "", + }, ), "SkipAction": dict( messages=[], - result={"level": STATUS_CODE["SKIP"], "id": "SKIP", "message": "SKIP MESSAGE"}, + result={ + "level": STATUS_CODE["SKIP"], + "id": "SKIP", + "title": "Skip", + "description": "Action skip", + "diagnosis": "User skip", + "remediation": "move on", + }, ), "OverridableAction": dict( messages=[], result={ "level": STATUS_CODE["OVERRIDABLE"], "id": "OVERRIDABLE", - "message": "OVERRIDABLE MESSAGE", + "title": "Overridable", + "description": "Action override", + "diagnosis": "User override", + "remediation": "move on", }, ), "ErrorAction": dict( messages=[], - result={"level": STATUS_CODE["ERROR"], "id": "ERROR", "message": "ERROR MESSAGE"}, + result={ + "level": STATUS_CODE["ERROR"], + "id": "ERROR", + "title": "Error", + "description": "Action error", + "diagnosis": "User error", + "remediation": "move on", + }, ), }, True, [ - r"\(ERROR\) ErrorAction::ERROR - ERROR MESSAGE", - r"\(OVERRIDABLE\) OverridableAction::OVERRIDABLE - OVERRIDABLE MESSAGE", - r"\(SKIP\) SkipAction::SKIP - SKIP MESSAGE", - r"\(SUCCESS\) PreSubscription - All good!", + r"\(ERROR\) ErrorAction::ERROR - Error\n Description: Action error\n Diagnosis: User error\n Remediation: move on", + r"\(OVERRIDABLE\) OverridableAction::OVERRIDABLE - Overridable\n Description: Action override\n Diagnosis: User override\n Remediation: move on", + r"\(SKIP\) SkipAction::SKIP - Skip\n Description: Action skip\n Diagnosis: User skip\n Remediation: move on", + r"\(SUCCESS\) PreSubscription::SUCCESS - \[No further information given\]", ], ), ), @@ -372,7 +641,7 @@ def test_results_summary_ordering(results, include_all_reports, expected_results pattern.append(entry) pattern = ".*".join(pattern) - assert re.search(pattern, message, re.DOTALL) + assert re.search(pattern, message, re.DOTALL | re.MULTILINE) @pytest.mark.parametrize( @@ -382,83 +651,201 @@ def test_results_summary_ordering(results, include_all_reports, expected_results ( { "PreSubscription2": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE"}], - result={"level": STATUS_CODE["SKIP"], "id": "SKIPPED", "message": "SKIP MESSAGE"}, + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], + result={ + "level": STATUS_CODE["SKIP"], + "id": "SKIPPED", + "title": "Skip", + "description": "Action skip", + "diagnosis": "User skip", + "remediation": "move on", + }, ), "PreSubscription1": dict( messages=[], result={ "level": STATUS_CODE["OVERRIDABLE"], "id": "SOME_OVERRIDABLE", - "message": "OVERRIDABLE MESSAGE", + "title": "Override", + "description": "Action override", + "diagnosis": "User override", + "remediation": "move on", }, ), }, False, [ - "(OVERRIDABLE) PreSubscription1::SOME_OVERRIDABLE - OVERRIDABLE MESSAGE", - "(SKIP) PreSubscription2::SKIPPED - SKIP MESSAGE", - "(WARNING) PreSubscription2::WARNING_ID - WARNING MESSAGE", + "(OVERRIDABLE) PreSubscription1::SOME_OVERRIDABLE - Override\n Description: Action override\n Diagnosis: User override\n Remediation: move on", + "(SKIP) PreSubscription2::SKIPPED - Skip\n Description: Action skip\n Diagnosis: User skip\n Remediation: move on", + "(WARNING) PreSubscription2::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on", ], ), ( { "SkipAction": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE 1"}], - result={"level": STATUS_CODE["SKIP"], "id": "SKIP", "message": "SKIP MESSAGE"}, + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], + result={ + "level": STATUS_CODE["SKIP"], + "id": "SKIP", + "title": "Skip", + "description": "Action skip", + "diagnosis": "User skip", + "remediation": "move on", + }, ), "OverridableAction": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE 2"}], + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], result={ "level": STATUS_CODE["OVERRIDABLE"], "id": "OVERRIDABLE", - "message": "OVERRIDABLE MESSAGE", + "title": "Overridable", + "description": "Action overridable", + "diagnosis": "User overridable", + "remediation": "move on", }, ), "ErrorAction": dict( messages=[], - result={"level": STATUS_CODE["ERROR"], "id": "ERROR", "message": "ERROR MESSAGE"}, + result={ + "level": STATUS_CODE["ERROR"], + "id": "ERROR", + "title": "Error", + "description": "Action error", + "diagnosis": "User error", + "remediation": "move on", + }, ), }, False, [ - "(ERROR) ErrorAction::ERROR - ERROR MESSAGE", - "(OVERRIDABLE) OverridableAction::OVERRIDABLE - OVERRIDABLE MESSAGE", - "(SKIP) SkipAction::SKIP - SKIP MESSAGE", - "(WARNING) SkipAction::WARNING_ID - WARNING MESSAGE 1", - "(WARNING) OverridableAction::WARNING_ID - WARNING MESSAGE 2", + "(ERROR) ErrorAction::ERROR - Error\n Description: Action error\n Diagnosis: User error\n Remediation: move on", + "(OVERRIDABLE) OverridableAction::OVERRIDABLE - Overridable\n Description: Action overridable\n Diagnosis: User overridable\n Remediation: move on", + "(SKIP) SkipAction::SKIP - Skip\n Description: Action skip\n Diagnosis: User skip\n Remediation: move on", + "(WARNING) SkipAction::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on", + "(WARNING) OverridableAction::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on", ], ), # Message order with `include_all_reports` set to True. ( { "PreSubscription": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE 1"}], - result={"level": STATUS_CODE["SUCCESS"], "id": None, "message": "All good!"}, + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], + result={ + "level": STATUS_CODE["SUCCESS"], + "id": "SUCCESS", + "title": "", + "description": "", + "diagnosis": "", + "remediation": "", + }, ), "SkipAction": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE 2"}], - result={"level": STATUS_CODE["SKIP"], "id": "SKIP", "message": "SKIP MESSAGE"}, + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], + result={ + "level": STATUS_CODE["SKIP"], + "id": "SKIP", + "title": "Skip", + "description": "Action skip", + "diagnosis": "User skip", + "remediation": "move on", + }, ), "OverridableAction": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE 3"}], - result={"level": STATUS_CODE["OVERRIDABLE"], "id": "OVERRIDABLE", "message": "OVERRIDABLE MESSAGE"}, + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], + result={ + "level": STATUS_CODE["OVERRIDABLE"], + "id": "OVERRIDABLE", + "title": "Overridable", + "description": "Action overridable", + "diagnosis": "User overridable", + "remediation": "move on", + }, ), "ErrorAction": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE 4"}], - result={"level": STATUS_CODE["ERROR"], "id": "ERROR", "message": "ERROR MESSAGE"}, + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], + result={ + "level": STATUS_CODE["ERROR"], + "id": "ERROR", + "title": "Error", + "description": "Action error", + "diagnosis": "User error", + "remediation": "move on", + }, ), }, True, [ - "(ERROR) ErrorAction::ERROR - ERROR MESSAGE", - "(OVERRIDABLE) OverridableAction::OVERRIDABLE - OVERRIDABLE MESSAGE", - "(SKIP) SkipAction::SKIP - SKIP MESSAGE", - "(WARNING) PreSubscription::WARNING_ID - WARNING MESSAGE 1", - "(WARNING) SkipAction::WARNING_ID - WARNING MESSAGE 2", - "(WARNING) OverridableAction::WARNING_ID - WARNING MESSAGE 3", - "(WARNING) ErrorAction::WARNING_ID - WARNING MESSAGE 4", - "(SUCCESS) PreSubscription - All good!", + "(ERROR) ErrorAction::ERROR - Error\n Description: Action error\n Diagnosis: User error\n Remediation: move on", + "(OVERRIDABLE) OverridableAction::OVERRIDABLE - Overridable\n Description: Action overridable\n Diagnosis: User overridable\n Remediation: move on", + "(SKIP) SkipAction::SKIP - Skip\n Description: Action skip\n Diagnosis: User skip\n Remediation: move on", + "(WARNING) PreSubscription::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on", + "(WARNING) SkipAction::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on", + "(WARNING) OverridableAction::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on", + "(WARNING) ErrorAction::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on", + "(SUCCESS) PreSubscription::SUCCESS - [No further information given]", ], ), ), @@ -475,9 +862,8 @@ def test_messages_summary_ordering(results, include_all_reports, expected_result # Prove that all the messages occurred for expected in expected_results: - assert expected in caplog_messages - - assert len(expected_results) == len(caplog_messages) + message = "\n".join(caplog_messages) + assert expected in message @pytest.mark.parametrize( @@ -486,42 +872,122 @@ def test_messages_summary_ordering(results, include_all_reports, expected_result ( { "ErrorAction": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE"}], - result={"level": STATUS_CODE["ERROR"], "id": "ERROR", "message": "ERROR MESSAGE"}, + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], + result={ + "level": STATUS_CODE["ERROR"], + "id": "ERROR", + "title": "Error", + "description": "Action error", + "diagnosis": "User error", + "remediation": "move on", + }, ) }, - "%s(ERROR) ErrorAction::ERROR - ERROR MESSAGE%s" % (bcolors.FAIL, bcolors.ENDC), - "%s(WARNING) ErrorAction::WARNING_ID - WARNING MESSAGE%s" % (bcolors.WARNING, bcolors.ENDC), + "{begin}(ERROR) ErrorAction::ERROR - Error\n Description: Action error\n Diagnosis: User error\n Remediation: move on{end}".format( + begin=bcolors.FAIL, end=bcolors.ENDC + ), + "{begin}(WARNING) ErrorAction::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on{end}".format( + begin=bcolors.WARNING, end=bcolors.ENDC + ), ), ( { "OverridableAction": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE"}], - result={"level": STATUS_CODE["OVERRIDABLE"], "id": "OVERRIDABLE", "message": "OVERRIDABLE MESSAGE"}, + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], + result={ + "level": STATUS_CODE["OVERRIDABLE"], + "id": "OVERRIDABLE", + "title": "Overridable", + "description": "Action overridable", + "diagnosis": "User overridable", + "remediation": "move on", + }, ) }, - "%s(OVERRIDABLE) OverridableAction::OVERRIDABLE - OVERRIDABLE MESSAGE%s" % (bcolors.FAIL, bcolors.ENDC), - "%s(WARNING) OverridableAction::WARNING_ID - WARNING MESSAGE%s" % (bcolors.WARNING, bcolors.ENDC), + "{begin}(OVERRIDABLE) OverridableAction::OVERRIDABLE - Overridable\n Description: Action overridable\n Diagnosis: User overridable\n Remediation: move on{end}".format( + begin=bcolors.FAIL, end=bcolors.ENDC + ), + "{begin}(WARNING) OverridableAction::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on{end}".format( + begin=bcolors.WARNING, end=bcolors.ENDC + ), ), ( { "SkipAction": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE"}], - result={"level": STATUS_CODE["SKIP"], "id": "SKIP", "message": "SKIP MESSAGE"}, + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], + result={ + "level": STATUS_CODE["SKIP"], + "id": "SKIP", + "title": "Skip", + "description": "Action skip", + "diagnosis": "User skip", + "remediation": "move on", + }, ) }, - "%s(SKIP) SkipAction::SKIP - SKIP MESSAGE%s" % (bcolors.FAIL, bcolors.ENDC), - "%s(WARNING) SkipAction::WARNING_ID - WARNING MESSAGE%s" % (bcolors.WARNING, bcolors.ENDC), + "{begin}(SKIP) SkipAction::SKIP - Skip\n Description: Action skip\n Diagnosis: User skip\n Remediation: move on{end}".format( + begin=bcolors.FAIL, end=bcolors.ENDC + ), + "{begin}(WARNING) SkipAction::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on{end}".format( + begin=bcolors.WARNING, end=bcolors.ENDC + ), ), ( { "SuccessfulAction": dict( - messages=[{"level": STATUS_CODE["WARNING"], "id": "WARNING_ID", "message": "WARNING MESSAGE"}], - result={"level": STATUS_CODE["SUCCESS"], "id": "SUCCESSFUL", "message": "SUCCESSFUL MESSAGE"}, + messages=[ + { + "level": STATUS_CODE["WARNING"], + "id": "WARNING_ID", + "title": "Warning", + "description": "Action warning", + "diagnosis": "User warning", + "remediation": "move on", + } + ], + result={ + "level": STATUS_CODE["SUCCESS"], + "id": "SUCCESS", + "title": "", + "description": "", + "diagnosis": "", + "remediation": "", + }, ) }, - "%s(SUCCESS) SuccessfulAction::SUCCESSFUL - SUCCESSFUL MESSAGE%s" % (bcolors.OKGREEN, bcolors.ENDC), - "%s(WARNING) SuccessfulAction::WARNING_ID - WARNING MESSAGE%s" % (bcolors.WARNING, bcolors.ENDC), + "{begin}(SUCCESS) SuccessfulAction::SUCCESS - [No further information given]{end}".format( + begin=bcolors.OKGREEN, end=bcolors.ENDC + ), + "{begin}(WARNING) SuccessfulAction::WARNING_ID - Warning\n Description: Action warning\n Diagnosis: User warning\n Remediation: move on{end}".format( + begin=bcolors.WARNING, end=bcolors.ENDC + ), ), ), ) diff --git a/convert2rhel/unit_tests/actions/system_checks/convert2rhel_latest_test.py b/convert2rhel/unit_tests/actions/system_checks/convert2rhel_latest_test.py index 16ac1288b..9bb0e0b83 100644 --- a/convert2rhel/unit_tests/actions/system_checks/convert2rhel_latest_test.py +++ b/convert2rhel/unit_tests/actions/system_checks/convert2rhel_latest_test.py @@ -22,7 +22,7 @@ import pytest import six -from convert2rhel import actions, systeminfo, utils +from convert2rhel import actions, systeminfo, unit_tests, utils from convert2rhel.actions.system_checks import convert2rhel_latest @@ -52,6 +52,11 @@ def convert2rhel_latest_version_test(monkeypatch, tmpdir, request, global_system return marker["local_version"], marker["package_version"] +@pytest.fixture +def current_version(request): + return request.param + + class TestCheckConvert2rhelLatest: @pytest.mark.parametrize( ("convert2rhel_latest_version_test",), @@ -67,10 +72,24 @@ def test_convert2rhel_latest_offline( ): global_system_info.has_internet_access = False convert2rhel_latest_action.run() + expected = set( + ( + actions.ActionMessage( + level="WARNING", + id="CONVERT2RHEL_LATEST_CHECK_SKIP_NO_INTERNET", + title="Skipping convert2rhel latest version check", + description="Skipping the check because no internet connection has been detected.", + diagnosis=None, + remediation=None, + ), + ) + ) log_msg = "Skipping the check because no internet connection has been detected." assert log_msg in caplog.text assert convert2rhel_latest_action.result.level == actions.STATUS_CODE["SUCCESS"] + assert expected.issuperset(convert2rhel_latest_action.messages) + assert expected.issubset(convert2rhel_latest_action.messages) @pytest.mark.parametrize( ("convert2rhel_latest_version_test",), @@ -84,11 +103,43 @@ def test_convert2rhel_latest_offline( indirect=True, ) def test_convert2rhel_latest_action_error(self, convert2rhel_latest_action, convert2rhel_latest_version_test): - convert2rhel_latest_action.run() - assert convert2rhel_latest_action.result.id == "OUT_OF_DATE" - assert convert2rhel_latest_action.result.level == actions.STATUS_CODE["ERROR"] + local_version, package_version = convert2rhel_latest_version_test + if len(package_version) > 36: + + package_version = package_version[19:25] + else: + package_version = package_version[19:23] + + unit_tests.assert_actions_result( + convert2rhel_latest_action, + level="ERROR", + id="OUT_OF_DATE", + title="Outdated convert2rhel version detected", + description="An outdated convert2rhel version has been detected", + diagnosis=( + "You are currently running %s and the latest version of convert2rhel is %s.\n" + "Only the latest version is supported for conversion." % (local_version, package_version) + ), + remediation="If you want to ignore this check, then set the environment variable 'CONVERT2RHEL_ALLOW_OLDER_VERSION=1' to continue.", + ) + + @pytest.mark.parametrize( + ("convert2rhel_latest_version_test",), + ( + [{"local_version": "0.21", "package_version": "C2R convert2rhel-0:0.22-1.el7.noarch", "pmajor": "6"}], + [{"local_version": "0.21", "package_version": "C2R convert2rhel-0:1.10-1.el7.noarch", "pmajor": "6"}], + [{"local_version": "1.21.0", "package_version": "C2R convert2rhel-0:1.21.1-1.el7.noarch", "pmajor": "6"}], + [{"local_version": "1.21", "package_version": "C2R convert2rhel-0:1.21.1-1.el7.noarch", "pmajor": "6"}], + [{"local_version": "1.21.1", "package_version": "C2R convert2rhel-0:1.22-1.el7.noarch", "pmajor": "6"}], + ), + indirect=True, + ) + def test_convert2rhel_latest_action_outdated_version( + self, convert2rhel_latest_action, convert2rhel_latest_version_test + ): + convert2rhel_latest_action.run() local_version, package_version = convert2rhel_latest_version_test if len(package_version) > 36: @@ -96,13 +147,24 @@ def test_convert2rhel_latest_action_error(self, convert2rhel_latest_action, conv package_version = package_version[19:25] else: package_version = package_version[19:23] - msg = ( - "You are currently running %s and the latest version of Convert2RHEL is %s.\n" - "Only the latest version is supported for conversion. If you want to ignore" - " this check, then set the environment variable 'CONVERT2RHEL_ALLOW_OLDER_VERSION=1' to continue." - % (local_version, package_version) + + expected = set( + ( + actions.ActionMessage( + level="WARNING", + id="OUTDATED_CONVERT2RHEL_VERSION", + title="Outdated convert2rhel version detected", + description="An outdated convert2rhel version has been detected", + diagnosis=( + "You are currently running %s and the latest version of convert2rhel is %s.\n" + "We encourage you to update to the latest version." % (local_version, package_version) + ), + remediation=None, + ), + ) ) - assert convert2rhel_latest_action.result.message == msg + assert expected.issuperset(convert2rhel_latest_action.messages) + assert expected.issubset(convert2rhel_latest_action.messages) @pytest.mark.parametrize( ("convert2rhel_latest_version_test",), @@ -162,7 +224,7 @@ def test_convert2rhel_latest_log_check_env( else: package_version = package_version[19:23] log_msg = ( - "You are currently running %s and the latest version of Convert2RHEL is %s.\n" + "You are currently running %s and the latest version of convert2rhel is %s.\n" "'CONVERT2RHEL_ALLOW_OLDER_VERSION' environment variable detected, continuing conversion" % (local_version, package_version) ) @@ -188,7 +250,7 @@ def test_convert2rhel_latest_log_check_env( def test_c2r_up_to_date(self, caplog, monkeypatch, convert2rhel_latest_action, convert2rhel_latest_version_test): convert2rhel_latest_action.run() - log_msg = "Latest available Convert2RHEL version is installed." + log_msg = "Latest available convert2rhel version is installed." assert log_msg in caplog.text @pytest.mark.parametrize( @@ -204,14 +266,30 @@ def test_c2r_up_to_date_repoquery_error( self, caplog, monkeypatch, convert2rhel_latest_action, convert2rhel_latest_version_test ): monkeypatch.setattr(utils, "run_subprocess", mock.Mock(return_value=("Repoquery did not run", 1))) - + expected = set( + ( + actions.ActionMessage( + level="WARNING", + id="CONVERT2RHEL_LATEST_CHECK_SKIP", + title="convert2rhel latest version check skip", + description="Skipping the convert2hel latest version check", + diagnosis=( + "Couldn't check if the current installed convert2rhel is the latest version.\n" + "repoquery failed with the following output:\nRepoquery did not run" + ), + remediation=None, + ), + ) + ) convert2rhel_latest_action.run() log_msg = ( - "Couldn't check if the current installed Convert2RHEL is the latest version.\n" + "Couldn't check if the current installed convert2rhel is the latest version.\n" "repoquery failed with the following output:\nRepoquery did not run" ) assert log_msg in caplog.text + assert expected.issuperset(convert2rhel_latest_action.messages) + assert expected.issubset(convert2rhel_latest_action.messages) @pytest.mark.parametrize( ("convert2rhel_latest_version_test",), @@ -241,12 +319,8 @@ def test_c2r_up_to_date_repoquery_error( indirect=True, ) def test_c2r_up_to_date_multiple_packages(self, convert2rhel_latest_action, convert2rhel_latest_version_test): - convert2rhel_latest_action.run() - assert convert2rhel_latest_action.result.id == "OUT_OF_DATE" - assert convert2rhel_latest_action.result.level == actions.STATUS_CODE["ERROR"] - local_version, package_version = convert2rhel_latest_version_test if len(package_version) > 110: @@ -254,29 +328,67 @@ def test_c2r_up_to_date_multiple_packages(self, convert2rhel_latest_action, conv else: package_version = package_version[93:97] - msg = ( - "You are currently running %s and the latest version of Convert2RHEL is %s.\n" - "Only the latest version is supported for conversion. If you want to ignore" - " this check, then set the environment variable 'CONVERT2RHEL_ALLOW_OLDER_VERSION=1' to continue." - % (local_version, package_version) + unit_tests.assert_actions_result( + convert2rhel_latest_action, + level="ERROR", + id="OUT_OF_DATE", + title="Outdated convert2rhel version detected", + description="An outdated convert2rhel version has been detected", + diagnosis=( + "You are currently running %s and the latest version of convert2rhel is %s.\n" + "Only the latest version is supported for conversion." % (local_version, package_version) + ), + remediation="If you want to ignore this check, then set the environment variable 'CONVERT2RHEL_ALLOW_OLDER_VERSION=1' to continue.", ) - assert convert2rhel_latest_action.result.message == msg @pytest.mark.parametrize( - ("convert2rhel_latest_version_test",), ( - [{"local_version": "0.17.0", "package_version": "C2R convert2rhel-0:0.18.0-1.el7.noarch", "pmajor": "8"}], - [{"local_version": "0.17", "package_version": "C2R convert2rhel-0:0.18.0-1.el7.noarch", "pmajor": "8"}], - [{"local_version": "0.17.0", "package_version": "C2R convert2rhel-0:0.18-1.el7.noarch", "pmajor": "8"}], + "convert2rhel_latest_version_test", + "current_version", + ), + ( + [ + {"local_version": "0.17.0", "package_version": "C2R convert2rhel-0:0.18.0-1.el7.noarch", "pmajor": "8"}, + "0.18.0", + ], + [ + {"local_version": "0.17", "package_version": "C2R convert2rhel-0:0.18.0-1.el7.noarch", "pmajor": "8"}, + "0.18.0", + ], + [ + {"local_version": "0.17.0", "package_version": "C2R convert2rhel-0:0.18-1.el7.noarch", "pmajor": "8"}, + "0.18", + ], ), indirect=True, ) def test_c2r_up_to_date_deprecated_env_var( - self, caplog, monkeypatch, convert2rhel_latest_action, convert2rhel_latest_version_test + self, caplog, monkeypatch, convert2rhel_latest_action, convert2rhel_latest_version_test, current_version ): env = {"CONVERT2RHEL_UNSUPPORTED_VERSION": 1} monkeypatch.setattr(os, "environ", env) - + expected = set( + ( + actions.ActionMessage( + level="WARNING", + id="DEPRECATED_ENVIRONMENT_VARIABLE", + title="Deprecated environment variable", + description="A deprecated environment variable has been detected", + diagnosis="You are using the deprecated 'CONVERT2RHEL_UNSUPPORTED_VERSION'", + remediation="Please switch to the 'CONVERT2RHEL_ALLOW_OLDER_VERSION' environment variable instead", + ), + actions.ActionMessage( + level="WARNING", + id="ALLOW_OLDER_VERSION_ENVIRONMENT_VARIABLE", + title="Outdated convert2rhel version detected", + description="An outdated convert2rhel version has been detected", + diagnosis="You are currently running %s and the latest version of convert2rhel is %s.\n" + "'CONVERT2RHEL_ALLOW_OLDER_VERSION' environment variable detected, continuing conversion" + % (convert2rhel_latest_version_test[0], current_version), + remediation=None, + ), + ) + ) convert2rhel_latest_action.run() log_msg = ( @@ -284,5 +396,6 @@ def test_c2r_up_to_date_deprecated_env_var( " environment variable. Please switch to 'CONVERT2RHEL_ALLOW_OLDER_VERSION'" " instead." ) - + assert expected.issuperset(convert2rhel_latest_action.messages) + assert expected.issubset(convert2rhel_latest_action.messages) assert log_msg in caplog.text diff --git a/convert2rhel/unit_tests/actions/system_checks/custom_repos_are_valid_test.py b/convert2rhel/unit_tests/actions/system_checks/custom_repos_are_valid_test.py index d0df5df0d..8b450d9e8 100644 --- a/convert2rhel/unit_tests/actions/system_checks/custom_repos_are_valid_test.py +++ b/convert2rhel/unit_tests/actions/system_checks/custom_repos_are_valid_test.py @@ -65,16 +65,36 @@ def test_custom_repos_are_valid(self): @unit_tests.mock( custom_repos_are_valid, "call_yum_cmd", - CallYumCmdMocked(ret_code=1, ret_string="Abcdef"), + CallYumCmdMocked(ret_code=1, ret_string="YUM/DNF failed"), ) @unit_tests.mock(custom_repos_are_valid, "logger", GetLoggerMocked()) @unit_tests.mock(custom_repos_are_valid.tool_opts, "no_rhsm", True) def test_custom_repos_are_invalid(self): self.custom_repos_are_valid_action.run() - self.assertEqual(len(custom_repos_are_valid.logger.info_msgs), 0) - self.assertEqual(self.custom_repos_are_valid_action.result.level, actions.STATUS_CODE["ERROR"]) - self.assertEqual(self.custom_repos_are_valid_action.result.id, "UNABLE_TO_ACCESS_REPOSITORIES") - self.assertIn( - "Unable to access the repositories passed through the --enablerepo option. ", - self.custom_repos_are_valid_action.result.message, + unit_tests.assert_actions_result( + self.custom_repos_are_valid_action, + level="ERROR", + id="UNABLE_TO_ACCESS_REPOSITORIES", + title="Unable to access repositories", + description="Access could not be made to the custom repositories.", + diagnosis="Unable to access the repositories passed through the --enablerepo option.", + remediation="For more details, see YUM/DNF output:\nYUM/DNF failed", + ) + + @unit_tests.mock(custom_repos_are_valid.tool_opts, "no_rhsm", False) + def test_custom_repos_are_valid_skip(self): + self.custom_repos_are_valid_action.run() + expected = set( + ( + actions.ActionMessage( + level="INFO", + id="CUSTOM_REPOSITORIES_ARE_VALID_CHECK_SKIP", + title="Skipping the custom repos are valid check", + description="Skipping the check of repositories due to the use of RHSM for the conversion.", + diagnosis=None, + remediation=None, + ), + ) ) + assert expected.issuperset(self.custom_repos_are_valid_action.messages) + assert expected.issubset(self.custom_repos_are_valid_action.messages) diff --git a/convert2rhel/unit_tests/actions/system_checks/dbus_test.py b/convert2rhel/unit_tests/actions/system_checks/dbus_test.py index 956830c62..72e286e3c 100644 --- a/convert2rhel/unit_tests/actions/system_checks/dbus_test.py +++ b/convert2rhel/unit_tests/actions/system_checks/dbus_test.py @@ -17,7 +17,7 @@ import pytest -from convert2rhel import unit_tests +from convert2rhel import actions, unit_tests from convert2rhel.actions.system_checks import dbus @@ -59,9 +59,29 @@ def test_check_dbus_is_running_not_running(monkeypatch, global_tool_opts, global dbus_is_running_action, level="ERROR", id="DBUS_DAEMON_NOT_RUNNING", - message=( - "Could not find a running DBus Daemon which is needed to" - " register with subscription manager.\nPlease start dbus using `systemctl" - " start dbus`" - ), + description="The Dbus daemon is not running", + diagnosis="Could not find a running DBus Daemon which is needed to register with subscription manager.", + remediation="Please start dbus using `systemctl start dbus`", ) + + +def test_check_dbus_is_running_warning_message( + monkeypatch, global_tool_opts, global_system_info, dbus_is_running_action +): + monkeypatch.setattr(dbus, "tool_opts", global_tool_opts) + global_tool_opts.no_rhsm = True + dbus_is_running_action.run() + expected = set( + ( + actions.ActionMessage( + level="WARNING", + id="DBUS_IS_RUNNING_CHECK_SKIP", + title="Skipping the dbus is running check", + description="Skipping the check because we have been asked not to subscribe this system to RHSM.", + diagnosis=None, + remediation=None, + ), + ) + ) + assert expected.issuperset(dbus_is_running_action.messages) + assert expected.issubset(dbus_is_running_action.messages) diff --git a/convert2rhel/unit_tests/actions/system_checks/efi_test.py b/convert2rhel/unit_tests/actions/system_checks/efi_test.py index 04403e768..1a5c6a8ff 100644 --- a/convert2rhel/unit_tests/actions/system_checks/efi_test.py +++ b/convert2rhel/unit_tests/actions/system_checks/efi_test.py @@ -43,11 +43,14 @@ def _check_efi_detection_log(self, efi_detected=True): self.assertIn("BIOS detected.", efi.logger.info_msgs) self.assertNotIn("UEFI detected.", efi.logger.info_msgs) - def _check_efi_critical(self, id, critical_msg): + def _check_efi_critical(self, id, title, description, diagnosis, remediation): self.efi_action.run() self.assertEqual(self.efi_action.result.level, actions.STATUS_CODE["ERROR"]) self.assertEqual(self.efi_action.result.id, id) - self.assertEqual(self.efi_action.result.message, critical_msg) + self.assertEqual(self.efi_action.result.title, title) + self.assertEqual(self.efi_action.result.description, description) + self.assertEqual(self.efi_action.result.diagnosis, diagnosis) + self.assertEqual(self.efi_action.result.remediation, remediation) self._check_efi_detection_log(True) @unit_tests.mock(grub, "is_efi", lambda: True) @@ -63,7 +66,11 @@ def _check_efi_critical(self, id, critical_msg): ) def test_check_efi_efi_detected_without_efibootmgr(self): self._check_efi_critical( - "EFIBOOTMGR_NOT_FOUND", "Install efibootmgr to continue converting the UEFI-based system." + "EFIBOOTMGR_NOT_FOUND", + "EFI boot manager not found", + "The EFI boot manager could not be found.", + "The EFI boot manager tool - efibootmgr could not be found on your system", + "Install efibootmgr to continue converting the UEFI-based system.", ) @unit_tests.mock(grub, "is_efi", lambda: True) @@ -78,7 +85,13 @@ def test_check_efi_efi_detected_without_efibootmgr(self): EFIBootInfoMocked(exception=grub.BootloaderError("errmsg")), ) def test_check_efi_efi_detected_non_intel(self): - self._check_efi_critical("NON_x86_64", "Only x86_64 systems are supported for UEFI conversions.") + self._check_efi_critical( + "NON_x86_64", + "None x86_64 system detected", + "Only x86_64 systems are supported for UEFI conversions.", + None, + None, + ) @unit_tests.mock(grub, "is_efi", lambda: True) @unit_tests.mock(grub, "is_secure_boot", lambda: True) @@ -94,8 +107,10 @@ def test_check_efi_efi_detected_non_intel(self): def test_check_efi_efi_detected_secure_boot(self): self._check_efi_critical( "SECURE_BOOT_DETECTED", - "The conversion with secure boot is currently not possible.\n" - "To disable it, follow the instructions available in this article: https://access.redhat.com/solutions/6753681", + "Secure boot detected", + "Secure boot has been detected.", + "The conversion with secure boot is currently not possible.", + "To disable secure boot, follow the instructions available in this article: https://access.redhat.com/solutions/6753681", ) self.assertIn("Secure boot detected.", efi.logger.info_msgs) @@ -111,7 +126,13 @@ def test_check_efi_efi_detected_secure_boot(self): EFIBootInfoMocked(exception=grub.BootloaderError("errmsg")), ) def test_check_efi_efi_detected_bootloader_error(self): - self._check_efi_critical("BOOTLOADER_ERROR", "errmsg") + self._check_efi_critical( + "BOOTLOADER_ERROR", + "Bootloader error detected", + "An unknown bootloader error occurred, please look at the diagnosis for more information.", + "errmsg", + None, + ) @unit_tests.mock(grub, "is_efi", lambda: True) @unit_tests.mock(grub, "is_secure_boot", lambda: False) @@ -129,6 +150,24 @@ def test_check_efi_efi_detected_nofile_entry(self): ) self.assertIn(warn_msg, efi.logger.warning_msgs) + expected = set( + ( + actions.ActionMessage( + level="WARNING", + id="UEFI_BOOTLOADER_MISMATCH", + title="UEFI bootloader mismatch", + description="There was a UEFI bootloader mismatch.", + diagnosis=( + "The current UEFI bootloader '0002' is not referring to any binary UEFI" + " file located on local EFI System Partition (ESP)." + ), + remediation=None, + ), + ) + ) + assert expected.issuperset(self.efi_action.messages) + assert expected.issubset(self.efi_action.messages) + @unit_tests.mock(grub, "is_efi", lambda: True) @unit_tests.mock(grub, "is_secure_boot", lambda: False) @unit_tests.mock(efi.system_info, "arch", "x86_64") diff --git a/convert2rhel/unit_tests/actions/system_checks/is_loaded_kernel_latest_test.py b/convert2rhel/unit_tests/actions/system_checks/is_loaded_kernel_latest_test.py index 47a7bfe36..d2c97cac2 100644 --- a/convert2rhel/unit_tests/actions/system_checks/is_loaded_kernel_latest_test.py +++ b/convert2rhel/unit_tests/actions/system_checks/is_loaded_kernel_latest_test.py @@ -51,7 +51,6 @@ def test_is_loaded_kernel_latest_skip_on_not_latest_ol( message = ( "Skipping the check because there are no publicly available Oracle Linux Server 8.6 repositories available." ) - is_loaded_kernel_latest_action.run() assert message in caplog.records[-1].message @@ -82,7 +81,6 @@ def test_is_loaded_kernel_latest_eus_system_invalid_kernel_version( package_name, tmpdir, monkeypatch, - caplog, is_loaded_kernel_latest_action, ): fake_reposdir_path = str(tmpdir) @@ -123,37 +121,49 @@ def test_is_loaded_kernel_latest_eus_system_invalid_kernel_version( is_loaded_kernel_latest_action.run() - repoquery_kernel_version = repoquery_version.split("\t")[2] - uname_kernel_version = uname_version.rsplit(".", 1)[0] - assert is_loaded_kernel_latest_action.result.id == "INVALID_KERNEL_VERSION" - assert is_loaded_kernel_latest_action.result.level == actions.STATUS_CODE["ERROR"] - assert ( - "The version of the loaded kernel is different from the latest version in repositories defined in the %s folder" - % fake_reposdir_path - in is_loaded_kernel_latest_action.result.message - ) - assert ( - "Latest kernel version available in baseos: %s\n" % repoquery_kernel_version - in is_loaded_kernel_latest_action.result.message + unit_tests.assert_actions_result( + is_loaded_kernel_latest_action, + id="INVALID_KERNEL_VERSION", + level="ERROR", + title="Invalid kernel version detected", + description="The loaded kernel version mismatch the latest one available in repositories defined in the %s folder" + % fake_reposdir_path, + diagnosis="The version of the loaded kernel is different from the latest version in repositories defined in the %s folder" + % fake_reposdir_path, + remediation="To proceed with the conversion, update the kernel version by executing the following step:\n\n", ) - assert "Loaded kernel version: %s\n" % uname_kernel_version in is_loaded_kernel_latest_action.result.message @pytest.mark.parametrize( - ("repoquery_version", "uname_version", "return_code", "package_name", "expected"), + ( + "repoquery_version", + "uname_version", + "return_code", + "package_name", + "title", + "description", + "diagnosis", + "remediation", + ), ( ( "C2R\t1634146676\t1-1.01-5.02\tbaseos", "2-1.01-5.02", 0, "kernel-core", + "Invalid kernel package found", + "Please refer to the diagnosis for further information", "The package names ('kernel-core-1' and 'kernel-core-2') do not match. Can only compare versions for the same packages.", + None, ), ( "C2R\t1634146676\t1 .01-5.02\tbaseos", "1 .01-5.03", 0, "kernel-core", + "Invalid kernel package found", + "Please refer to the diagnosis for further information", "Invalid package - kernel-core-1 .01-5.02, packages need to be in one of the following formats: NEVRA, NEVR, NVRA, NVR, ENVRA, ENVR.", + None, ), ), ) @@ -166,7 +176,10 @@ def test_is_loaded_kernel_latest_invalid_kernel_package_dnf( uname_version, return_code, package_name, - expected, + title, + description, + diagnosis, + remediation, monkeypatch, is_loaded_kernel_latest_action, ): @@ -199,30 +212,47 @@ def test_is_loaded_kernel_latest_invalid_kernel_package_dnf( ) is_loaded_kernel_latest_action.run() - unit_tests.assert_actions_result( is_loaded_kernel_latest_action, level="ERROR", id="INVALID_KERNEL_PACKAGE", - message=expected, + title=title, + description=description, + diagnosis=diagnosis, + remediation=remediation, ) @pytest.mark.parametrize( - ("repoquery_version", "uname_version", "return_code", "package_name", "expected"), + ( + "repoquery_version", + "uname_version", + "return_code", + "package_name", + "title", + "description", + "diagnosis", + "remediation", + ), ( ( "C2R\t1634146676\t1-1.01-5.02\tbaseos", "2-1.01-5.02", 0, "kernel", + "Invalid kernel package found", + "Please refer to the diagnosis for further information", "The following field(s) are invalid - release : 1.01-5", + None, ), ( "C2R\t1634146676\t1 .01-5.02\tbaseos", "1 .01-5.03", 0, "kernel", + "Invalid kernel package found", + "Please refer to the diagnosis for further information", "The following field(s) are invalid - version : 1 .01", + None, ), ), ) @@ -235,7 +265,10 @@ def test_is_loaded_kernel_latest_invalid_kernel_package_yum( uname_version, return_code, package_name, - expected, + title, + description, + diagnosis, + remediation, monkeypatch, is_loaded_kernel_latest_action, ): @@ -273,7 +306,10 @@ def test_is_loaded_kernel_latest_invalid_kernel_package_yum( is_loaded_kernel_latest_action, level="ERROR", id="INVALID_KERNEL_PACKAGE", - message=expected, + title=title, + description=description, + diagnosis=diagnosis, + remediation=remediation, ) @centos8 @@ -325,9 +361,23 @@ def test_is_loaded_kernel_latest_eus_system_no_connection( ): monkeypatch.setattr(is_loaded_kernel_latest, "get_hardcoded_repofiles_dir", value=lambda: str(tmpdir)) monkeypatch.setattr(is_loaded_kernel_latest.system_info, "has_internet_access", False) + expected = set( + ( + actions.ActionMessage( + level="WARNING", + id="IS_LOADED_KERNEL_LATEST_CHECK_SKIP", + title="Skipping the is loaded kernel latest check", + description="Skipping the check as no internet connection has been detected.", + diagnosis=None, + remediation=None, + ), + ) + ) is_loaded_kernel_latest_action.run() assert "Skipping the check as no internet connection has been detected." in caplog.records[-1].message + assert expected.issuperset(is_loaded_kernel_latest_action.messages) + assert expected.issubset(is_loaded_kernel_latest_action.messages) @centos8 @pytest.mark.parametrize( @@ -335,21 +385,43 @@ def test_is_loaded_kernel_latest_eus_system_no_connection( "repoquery_stdout", "return_code", "unsupported_skip", - "expected", + "level", + "id", + "title", + "description", + "diagnosis", + "remediation", ), ( pytest.param( "", 0, "1", - "Detected 'CONVERT2RHEL_UNSUPPORTED_SKIP_KERNEL_CURRENCY_CHECK' environment variable", + "WARNING", + "UNSUPPORTED_SKIP_KERNEL_CURRENCY_CHECK_DETECTED", + "Skipping the kernel currency check", + ( + "Detected 'CONVERT2RHEL_UNSUPPORTED_SKIP_KERNEL_CURRENCY_CHECK' environment variable, we will skip " + "the kernel-core comparison.\n" + "Beware, this could leave your system in a broken state." + ), + None, + None, id="Unsupported skip with environment var set to 1", ), pytest.param( "", 1, None, - "Couldn't fetch the list of the most recent kernels available", + "WARNING", + "UNABLE_TO_FETCH_RECENT_KERNELS", + "Unable to fetch recent kernels", + ( + "Couldn't fetch the list of the most recent kernels available in " + "the repositories. Skipping the loaded kernel check." + ), + None, + None, id="Unsupported skip with environment var not set", ), ), @@ -360,7 +432,12 @@ def test_is_loaded_kernel_latest_unsupported_skip_warnings( repoquery_stdout, return_code, unsupported_skip, - expected, + level, + id, + title, + description, + diagnosis, + remediation, monkeypatch, caplog, is_loaded_kernel_latest_action, @@ -394,10 +471,22 @@ def test_is_loaded_kernel_latest_unsupported_skip_warnings( "environ", {"CONVERT2RHEL_UNSUPPORTED_SKIP_KERNEL_CURRENCY_CHECK": unsupported_skip}, ) - + expected_set = set( + ( + actions.ActionMessage( + level=level, + id=id, + title=title, + description=description, + diagnosis=diagnosis, + remediation=remediation, + ), + ) + ) is_loaded_kernel_latest_action.run() - - assert expected in caplog.records[-1].message + assert description in caplog.records[-1].message + assert expected_set.issuperset(is_loaded_kernel_latest_action.messages) + assert expected_set.issubset(is_loaded_kernel_latest_action.messages) @centos8 @pytest.mark.parametrize( @@ -405,16 +494,20 @@ def test_is_loaded_kernel_latest_unsupported_skip_warnings( "repoquery_version", "return_code", "package_name", - "unsupported_skip", - "expected", + "title", + "description", + "diagnosis", + "remediation", ), ( pytest.param( "", 0, "kernel-core", - None, + "Kernel currency check failed", + "Please refer to the diagnosis for further information", "Could not find any {0} from repositories to compare against the loaded kernel.", + "Please, check if you have any vendor repositories enabled to proceed with the conversion.\nIf you wish to ignore this message, set the environment variable 'CONVERT2RHEL_UNSUPPORTED_SKIP_KERNEL_CURRENCY_CHECK' to 1.", id="Repoquery failure without environment var", ), ), @@ -425,10 +518,11 @@ def test_is_loaded_kernel_latest_unsupported_skip_error( repoquery_version, return_code, package_name, - unsupported_skip, - expected, + title, + description, + remediation, + diagnosis, monkeypatch, - caplog, is_loaded_kernel_latest_action, ): run_subprocess_mocked = mock.Mock( @@ -456,9 +550,15 @@ def test_is_loaded_kernel_latest_unsupported_skip_error( value=run_subprocess_mocked, ) is_loaded_kernel_latest_action.run() - expected = expected.format(package_name) + diagnosis = diagnosis.format(package_name) unit_tests.assert_actions_result( - is_loaded_kernel_latest_action, level="ERROR", id="KERNEL_CURRENCY_CHECK_FAIL", message=expected + is_loaded_kernel_latest_action, + level="ERROR", + id="KERNEL_CURRENCY_CHECK_FAIL", + title=title, + description=description, + diagnosis=diagnosis, + remediation=remediation, ) @pytest.mark.parametrize( @@ -614,13 +714,12 @@ def test_is_loaded_kernel_latest_system_exit(self, monkeypatch, caplog, is_loade ) is_loaded_kernel_latest_action.run() - - repoquery_kernel_version = repoquery_version.split("\t")[2] - uname_kernel_version = uname_version.rsplit(".", 1)[0] - assert is_loaded_kernel_latest_action.result.id == "INVALID_KERNEL_VERSION" - assert is_loaded_kernel_latest_action.result.level == actions.STATUS_CODE["ERROR"] - assert ( - "Latest kernel version available in baseos: %s" % repoquery_kernel_version - in is_loaded_kernel_latest_action.result.message + unit_tests.assert_actions_result( + is_loaded_kernel_latest_action, + level="ERROR", + id="INVALID_KERNEL_VERSION", + title="Invalid kernel version detected", + description="The loaded kernel version mismatch the latest one available in the enabled system repositories", + diagnosis="The version of the loaded kernel is different from the latest version in the enabled system repositories.", + remediation="To proceed with the conversion, update the kernel version by executing the following step:", ) - assert "Loaded kernel version: %s\n\n" % uname_kernel_version in is_loaded_kernel_latest_action.result.message diff --git a/convert2rhel/unit_tests/actions/system_checks/package_updates_test.py b/convert2rhel/unit_tests/actions/system_checks/package_updates_test.py index ca0a198f1..7eb2996f6 100644 --- a/convert2rhel/unit_tests/actions/system_checks/package_updates_test.py +++ b/convert2rhel/unit_tests/actions/system_checks/package_updates_test.py @@ -17,11 +17,12 @@ __metaclass__ = type +import os import pytest import six -from convert2rhel import pkgmanager +from convert2rhel import actions, pkgmanager, unit_tests from convert2rhel.actions.system_checks import package_updates from convert2rhel.systeminfo import system_info from convert2rhel.unit_tests.conftest import centos8, oracle8 @@ -38,49 +39,167 @@ def package_updates_action(): @oracle8 def test_check_package_updates_skip_on_not_latest_ol(pretend_os, caplog, package_updates_action): - message = ( + diagnosis = ( "Skipping the check because there are no publicly available Oracle Linux Server 8.6 repositories available." ) + expected = set( + ( + actions.ActionMessage( + level="INFO", + id="PACKAGE_UPDATES_CHECK_SKIP_NO_PUBLIC_REPOSITORIES", + title="Skipping the package updates check", + description="Please refer to the diagnosis for further information", + diagnosis=diagnosis, + remediation=None, + ), + ) + ) package_updates_action.run() - assert message in caplog.records[-1].message + assert diagnosis in caplog.records[-1].message + assert expected.issuperset(package_updates_action.messages) + assert expected.issubset(package_updates_action.messages) -@pytest.mark.parametrize( - ("packages", "exception", "expected"), - ( - (["package-1", "package-2"], True, "The system has {0} package(s) not updated"), - ([], False, "System is up-to-date."), - ), -) @centos8 -def test_check_package_updates(pretend_os, packages, exception, expected, monkeypatch, caplog, package_updates_action): +def test_check_package_updates(pretend_os, monkeypatch, caplog, package_updates_action): + monkeypatch.setattr(package_updates, "get_total_packages_to_update", value=lambda reposdir: []) + + package_updates_action.run() + assert "System is up-to-date." in caplog.records[-1].message + + +@centos8 +def test_check_package_updates_not_up_to_date(pretend_os, monkeypatch, package_updates_action, caplog): + packages = ["package-1", "package-2"] + diagnosis = ( + "The system has 2 package(s) not updated based on the enabled system repositories.\n" + "List of packages to update: package-1 package-2.\n\n" + "Not updating the packages may cause the conversion to fail.\n" + "Consider updating the packages before proceeding with the conversion." + ) monkeypatch.setattr(package_updates, "get_total_packages_to_update", value=lambda reposdir: packages) + package_updates_action.run() + unit_tests.assert_actions_result( + package_updates_action, + level="SUCCESS", + id="OUT_OF_DATE_PACKAGES", + title="Outdated packages detected", + description=None, + diagnosis=None, + ) + + expected = set( + ( + actions.ActionMessage( + level="WARNING", + id="PACKAGE_NOT_UP_TO_DATE_MESSAGE", + title="Outdated packages detected", + description="Please refer to the diagnosis for further information", + diagnosis=diagnosis, + remediation=None, + ), + ) + ) + + assert diagnosis in caplog.records[-1].message + assert expected.issuperset(package_updates_action.messages) + assert expected.issubset(package_updates_action.messages) + +@centos8 +def test_check_package_updates_with_repoerror(pretend_os, monkeypatch, caplog, package_updates_action): + get_total_packages_to_update_mock = mock.Mock(side_effect=pkgmanager.RepoError("This is an error")) + monkeypatch.setattr(package_updates, "get_total_packages_to_update", value=get_total_packages_to_update_mock) + monkeypatch.setattr(package_updates, "get_total_packages_to_update", value=get_total_packages_to_update_mock) + diagnosis = ( + "There was an error while checking whether the installed packages are up-to-date. Having an updated system is" + " an important prerequisite for a successful conversion. Consider verifyng the system is up to date manually" + " before proceeding with the conversion. This is an error" + ) package_updates_action.run() - if exception: - expected = expected.format(len(packages)) + unit_tests.assert_actions_result( + package_updates_action, + level="SUCCESS", + id="PACKAGE_UP_TO_DATE_CHECK_FAIL", + title="Package up to date check fail", + description=None, + diagnosis=None, + remediation=None, + ) + expected = set( + ( + actions.ActionMessage( + level="WARNING", + id="PACKAGE_UP_TO_DATE_CHECK_MESSAGE", + title="Package up to date check fail", + description="Please refer to the diagnosis for further information", + diagnosis=diagnosis, + remediation=None, + ), + ) + ) - assert expected in caplog.records[-1].message + assert diagnosis in caplog.records[-1].message + assert expected.issuperset(package_updates_action.messages) + assert expected.issubset(package_updates_action.messages) -def test_check_package_updates_with_repoerror(monkeypatch, caplog, package_updates_action): - get_total_packages_to_update_mock = mock.Mock(side_effect=pkgmanager.RepoError) +@centos8 +def test_check_package_updates_with_repoerror_skip(pretend_os, monkeypatch, caplog, package_updates_action): + get_total_packages_to_update_mock = mock.Mock(side_effect=pkgmanager.RepoError("This is an error")) monkeypatch.setattr(package_updates, "get_total_packages_to_update", value=get_total_packages_to_update_mock) monkeypatch.setattr(package_updates, "get_total_packages_to_update", value=get_total_packages_to_update_mock) + monkeypatch.setattr( + os, + "environ", + {"CONVERT2RHEL_PACKAGE_UP_TO_DATE_CHECK_SKIP": 1}, + ) + diagnosis = ( + "There was an error while checking whether the installed packages are up-to-date. Having an updated system is" + " an important prerequisite for a successful conversion. Consider verifyng the system is up to date manually" + " before proceeding with the conversion. This is an error" + ) + expected = set( + ( + actions.ActionMessage( + level="WARNING", + id="PACKAGE_UP_TO_DATE_CHECK_MESSAGE", + title="Package up to date check fail", + description="Please refer to the diagnosis for further information", + diagnosis=diagnosis, + remediation=None, + ), + ) + ) package_updates_action.run() - # This is -2 because the last message is the error from the RepoError class. - assert ( - "There was an error while checking whether the installed packages are up-to-date." in caplog.records[-2].message - ) + + assert diagnosis in caplog.records[-1].message + assert expected.issuperset(package_updates_action.messages) + assert expected.issubset(package_updates_action.messages) @centos8 def test_check_package_updates_without_internet(pretend_os, tmpdir, monkeypatch, caplog, package_updates_action): monkeypatch.setattr(package_updates, "get_hardcoded_repofiles_dir", value=lambda: str(tmpdir)) - system_info.has_internet_access = False + monkeypatch.setattr(system_info, "has_internet_access", False) + description = "Skipping the check as no internet connection has been detected." + expected = set( + ( + actions.ActionMessage( + level="WARNING", + id="PACKAGE_UPDATES_CHECK_SKIP_NO_INTERNET", + title="Skipping the package updates check", + description=description, + diagnosis=None, + remediation=None, + ), + ) + ) package_updates_action.run() - assert "Skipping the check as no internet connection has been detected." in caplog.records[-1].message + assert description in caplog.records[-1].message + assert expected.issuperset(package_updates_action.messages) + assert expected.issubset(package_updates_action.messages) diff --git a/convert2rhel/unit_tests/actions/system_checks/readonly_mounts_test.py b/convert2rhel/unit_tests/actions/system_checks/readonly_mounts_test.py index 6c28d0987..3c6009ec6 100644 --- a/convert2rhel/unit_tests/actions/system_checks/readonly_mounts_test.py +++ b/convert2rhel/unit_tests/actions/system_checks/readonly_mounts_test.py @@ -84,14 +84,17 @@ def test_mounted_sys_is_readwrite(self): ) def test_mounted_are_readonly_mnt(self): self.readonly_mounts_action_mnt.run() - self.assertEqual(self.readonly_mounts_action_mnt.result.level, actions.STATUS_CODE["ERROR"]) - self.assertEqual(self.readonly_mounts_action_mnt.result.id, "MNT_DIR_READONLY_MOUNT") - self.assertEqual( - self.readonly_mounts_action_mnt.result.message, - ( + unit_tests.assert_actions_result( + self.readonly_mounts_action_mnt, + level="ERROR", + id="MNT_DIR_READONLY_MOUNT", + title="Read-only mount in /mnt directory", + description=( "Stopping conversion due to read-only mount to /mnt directory.\n" "Mount at a subdirectory of /mnt to have /mnt writeable." ), + diagnosis=None, + remediation=None, ) @unit_tests.mock(readonly_mounts, "logger", GetLoggerMocked()) @@ -108,12 +111,15 @@ def test_mounted_are_readonly_mnt(self): ) def test_mounted_are_readonly_sys(self): self.readonly_mounts_action_sys.run() - self.assertEqual(self.readonly_mounts_action_sys.result.level, actions.STATUS_CODE["ERROR"]) - self.assertEqual(self.readonly_mounts_action_sys.result.id, "SYS_DIR_READONLY_MOUNT") - self.assertEqual( - self.readonly_mounts_action_sys.result.message, - ( + unit_tests.assert_actions_result( + self.readonly_mounts_action_sys, + level="ERROR", + id="SYS_DIR_READONLY_MOUNT", + title="Read-only mount in /sys directory", + description=( "Stopping conversion due to read-only mount to /sys directory.\n" "Ensure mount point is writable before executing convert2rhel." ), + diagnosis=None, + remediation=None, ) 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 20d5523bf..b22ac5f9a 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 @@ -24,6 +24,11 @@ from convert2rhel import unit_tests from convert2rhel.actions.system_checks import rhel_compatible_kernel +from convert2rhel.actions.system_checks.rhel_compatible_kernel import ( + BAD_KERNEL_RELEASE_SUBSTRINGS, + COMPATIBLE_KERNELS_VERS, + KernelIncompatibleError, +) from convert2rhel.unit_tests import create_pkg_information from convert2rhel.unit_tests.conftest import centos8 from convert2rhel.utils import run_subprocess @@ -38,24 +43,16 @@ def rhel_compatible_kernel_action(): return rhel_compatible_kernel.RhelCompatibleKernel() -@pytest.mark.parametrize( - # i.e. _bad_kernel_version... - ("any_of_the_subchecks_is_true",), - ( - (True,), - (False,), - ), -) -def test_check_rhel_compatible_kernel_is_used( - any_of_the_subchecks_is_true, +def test_check_rhel_compatible_kernel_failure( monkeypatch, - caplog, rhel_compatible_kernel_action, ): monkeypatch.setattr( rhel_compatible_kernel, "_bad_kernel_version", - value=mock.Mock(return_value=any_of_the_subchecks_is_true), + value=mock.Mock( + side_effect=KernelIncompatibleError("UNEXPECTED_VERSION", "Bad kernel version", dict(fake_data="fake")) + ), ) monkeypatch.setattr( rhel_compatible_kernel, @@ -73,41 +70,74 @@ def test_check_rhel_compatible_kernel_is_used( "version", value=Version(major=1, minor=0), ) + monkeypatch.setattr( + rhel_compatible_kernel.system_info, + "name", + value="Kernel-core", + ) rhel_compatible_kernel_action.run() - if any_of_the_subchecks_is_true: - unit_tests.assert_actions_result( - rhel_compatible_kernel_action, - level="ERROR", - id="BOOTED_KERNEL_INCOMPATIBLE", - message=( - "The booted kernel version is incompatible with the standard RHEL kernel. " - "To proceed with the conversion, boot into a kernel that is available in the {0} {1} base repository" - " by executing the following steps:\n\n" - "1. Ensure that the {0} {1} base repository is enabled\n" - "2. Run: yum install kernel\n" - "3. (optional) Run: grubby --set-default " - '/boot/vmlinuz-`rpm -q --qf "%{{BUILDTIME}}\\t%{{EVR}}.%{{ARCH}}\\n" kernel | sort -nr | head -1 | cut -f2`\n' - "4. Reboot the machine and if step 3 was not applied choose the kernel" - " installed in step 2 manually".format( - rhel_compatible_kernel.system_info.name, - rhel_compatible_kernel.system_info.version.major, - ) - ), - ) - else: - assert "is compatible with RHEL" in caplog.records[-1].message + unit_tests.assert_actions_result( + rhel_compatible_kernel_action, + level="ERROR", + id="UNEXPECTED_VERSION", + title="Incompatible booted kernel version", + description="Please refer to the diagnosis for further information", + diagnosis="The booted kernel version is incompatible with the standard RHEL kernel", + remediation=( + "To proceed with the conversion, boot into a kernel that is available in the {0} {1} base repository" + " by executing the following steps:\n\n" + "1. Ensure that the {0} {1} base repository is enabled\n" + "2. Run: yum install kernel\n" + "3. (optional) Run: grubby --set-default " + '/boot/vmlinuz-`rpm -q --qf "%{{BUILDTIME}}\\t%{{EVR}}.%{{ARCH}}\\n" kernel | sort -nr | head -1 | cut -f2`\n' + "4. Reboot the machine and if step 3 was not applied choose the kernel" + " installed in step 2 manually".format( + rhel_compatible_kernel.system_info.name, rhel_compatible_kernel.system_info.version.major + ) + ), + ) + + +def test_rhel_compatible_kernel_success(monkeypatch, caplog, rhel_compatible_kernel_action): + monkeypatch.setattr( + rhel_compatible_kernel, + "_bad_kernel_version", + value=mock.Mock(return_value=False), + ) + monkeypatch.setattr( + rhel_compatible_kernel, + "_bad_kernel_substring", + value=mock.Mock(return_value=False), + ) + monkeypatch.setattr( + rhel_compatible_kernel, + "_bad_kernel_package_signature", + value=mock.Mock(return_value=False), + ) + Version = namedtuple("Version", ("major", "minor")) + monkeypatch.setattr( + rhel_compatible_kernel.system_info, + "version", + value=Version(major=1, minor=0), + ) + monkeypatch.setattr( + rhel_compatible_kernel.system_info, + "name", + value="Kernel-core", + ) + rhel_compatible_kernel_action.run() + + assert "is compatible with RHEL" in caplog.records[-1].message @pytest.mark.parametrize( ("kernel_release", "major_ver", "exp_return"), ( - ("5.11.0-7614-generic", None, True), ("3.10.0-1160.24.1.el7.x86_64", 7, False), - ("5.4.17-2102.200.13.el8uek.x86_64", 8, True), ("4.18.0-240.22.1.el8_3.x86_64", 8, False), ), ) -def test_bad_kernel_version(kernel_release, major_ver, exp_return, monkeypatch): +def test_bad_kernel_version_success(kernel_release, major_ver, exp_return, monkeypatch): Version = namedtuple("Version", ("major", "minor")) monkeypatch.setattr( rhel_compatible_kernel.system_info, @@ -117,18 +147,84 @@ def test_bad_kernel_version(kernel_release, major_ver, exp_return, monkeypatch): assert rhel_compatible_kernel._bad_kernel_version(kernel_release) == exp_return +@pytest.mark.parametrize( + ("kernel_release", "major_ver", "error_id", "template", "variables"), + ( + ( + "5.11.0-7614-generic", + None, + "UNEXPECTED_VERSION", + "Unexpected OS major version. Expected: {compatible_version}", + dict(compatible_version=COMPATIBLE_KERNELS_VERS.keys()), + ), + ( + "5.4.17-2102.200.13.el8uek.x86_64", + 8, + "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), + ), + ), +) +def test_bad_kernel_version_invalid_version(kernel_release, major_ver, error_id, template, variables, monkeypatch): + Version = namedtuple("Version", ("major", "minor")) + monkeypatch.setattr( + rhel_compatible_kernel.system_info, + "version", + value=Version(major=major_ver, minor=0), + ) + with pytest.raises(KernelIncompatibleError) as excinfo: + rhel_compatible_kernel._bad_kernel_version(kernel_release) + assert excinfo.value.error_id == error_id + assert excinfo.value.template == template + assert excinfo.value.variables == variables + + @pytest.mark.parametrize( ("kernel_release", "exp_return"), ( ("3.10.0-1160.24.1.el7.x86_64", False), - ("5.4.17-2102.200.13.el8uek.x86_64", True), - ("3.10.0-514.2.2.rt56.424.el7.x86_64", True), + ("5.04.0-1240.41.0.el8.x86_64", False), ), ) -def test_bad_kernel_substring(kernel_release, exp_return): +def test_bad_kernel_substring_success(kernel_release, exp_return): assert rhel_compatible_kernel._bad_kernel_substring(kernel_release) == exp_return +@pytest.mark.parametrize( + ("kernel_release", "error_id", "template", "variables"), + ( + ( + "5.4.17-2102.200.13.el8uek.x86_64", + "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, + ), + ), + ( + "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, + ), + ), + ), +) +def test_bad_kernel_substring_invalid_substring(kernel_release, error_id, template, variables, monkeypatch): + with pytest.raises(KernelIncompatibleError) as excinfo: + rhel_compatible_kernel._bad_kernel_substring(kernel_release) + assert excinfo.value.error_id == error_id + assert excinfo.value.template == template + assert excinfo.value.variables == variables + + @pytest.mark.parametrize( ("kernel_release", "kernel_pkg", "kernel_pkg_information", "get_installed_pkg_objects", "exp_return"), ( @@ -146,6 +242,48 @@ def test_bad_kernel_substring(kernel_release, exp_return): "yajl.x86_64", False, ), + ), +) +@centos8 +def test_bad_kernel_package_signature_success( + kernel_release, + kernel_pkg, + kernel_pkg_information, + get_installed_pkg_objects, + exp_return, + monkeypatch, + pretend_os, +): + run_subprocess_mocked = mock.Mock(spec=run_subprocess, return_value=(kernel_pkg, 0)) + monkeypatch.setattr(rhel_compatible_kernel, "run_subprocess", run_subprocess_mocked) + get_installed_pkg_objects_mocked = mock.Mock( + spec=rhel_compatible_kernel.get_installed_pkg_objects, return_value=[kernel_pkg] + ) + get_installed_pkg_information_mocked = mock.Mock(return_value=[kernel_pkg_information]) + monkeypatch.setattr( + rhel_compatible_kernel, + "get_installed_pkg_objects", + get_installed_pkg_objects_mocked, + ) + monkeypatch.setattr(rhel_compatible_kernel, "get_installed_pkg_information", get_installed_pkg_information_mocked) + assert rhel_compatible_kernel._bad_kernel_package_signature(kernel_release) == exp_return + run_subprocess_mocked.assert_called_with( + ["rpm", "-qf", "--qf", "%{VERSION}&%{RELEASE}&%{ARCH}&%{NAME}", "/boot/vmlinuz-%s" % kernel_release], + print_output=False, + ) + + +@pytest.mark.parametrize( + ( + "kernel_release", + "kernel_pkg", + "kernel_pkg_information", + "get_installed_pkg_objects", + "error_id", + "template", + "variables", + ), + ( ( "4.18.0-240.22.1.el8_3.x86_64", "4.18.0&240.22.1.el8_3&x86_64&kernel-core", @@ -158,17 +296,21 @@ def test_bad_kernel_substring(kernel_release, exp_return): fingerprint="somebadsig", ), "somepkgobj", - True, + "INVALID_KERNEL_PACKAGE_SIGNATURE", + "Custom kernel detected. The booted kernel needs to be signed by {os_vendor}.", + dict(os_vendor="CentOS"), ), ), ) @centos8 -def test_bad_kernel_package_signature( +def test_bad_kernel_package_signature_invalid_signature( kernel_release, kernel_pkg, kernel_pkg_information, get_installed_pkg_objects, - exp_return, + error_id, + template, + variables, monkeypatch, pretend_os, ): @@ -184,20 +326,36 @@ def test_bad_kernel_package_signature( get_installed_pkg_objects_mocked, ) monkeypatch.setattr(rhel_compatible_kernel, "get_installed_pkg_information", get_installed_pkg_information_mocked) - assert rhel_compatible_kernel._bad_kernel_package_signature(kernel_release) == exp_return + + with pytest.raises(KernelIncompatibleError) as excinfo: + rhel_compatible_kernel._bad_kernel_package_signature(kernel_release) + assert excinfo.value.error_id == error_id + assert excinfo.value.template == template + assert excinfo.value.variables == variables run_subprocess_mocked.assert_called_with( ["rpm", "-qf", "--qf", "%{VERSION}&%{RELEASE}&%{ARCH}&%{NAME}", "/boot/vmlinuz-%s" % kernel_release], print_output=False, ) +@pytest.mark.parametrize( + ("error_id", "template", "variables"), + ( + ( + "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"), + ), + ), +) @centos8 -def test_kernel_not_installed(pretend_os, caplog, monkeypatch): +def test_kernel_not_installed(pretend_os, error_id, template, variables, monkeypatch): run_subprocess_mocked = mock.Mock(spec=run_subprocess, return_value=(" ", 1)) monkeypatch.setattr(rhel_compatible_kernel, "run_subprocess", run_subprocess_mocked) - assert rhel_compatible_kernel._bad_kernel_package_signature("4.18.0-240.22.1.el8_3.x86_64") - log_message = ( - "The booted kernel /boot/vmlinuz-4.18.0-240.22.1.el8_3.x86_64 is not owned by any installed package." - " It needs to be owned by a package signed by CentOS." - ) - assert log_message in caplog.text + + with pytest.raises(KernelIncompatibleError) as excinfo: + rhel_compatible_kernel._bad_kernel_package_signature("4.18.0-240.22.1.el8_3.x86_64") + assert excinfo.value.error_id == error_id + assert excinfo.value.template == template + assert excinfo.value.variables == variables diff --git a/convert2rhel/unit_tests/actions/system_checks/tainted_kmods_test.py b/convert2rhel/unit_tests/actions/system_checks/tainted_kmods_test.py index 56d7a3d7a..3ba7de01d 100644 --- a/convert2rhel/unit_tests/actions/system_checks/tainted_kmods_test.py +++ b/convert2rhel/unit_tests/actions/system_checks/tainted_kmods_test.py @@ -55,14 +55,20 @@ def test_check_tainted_kmods(monkeypatch, command_return, is_error, tainted_kmod "run_subprocess", value=run_subprocess_mock, ) + tainted_kmods_action.run() + if is_error: - tainted_kmods_action.run() unit_tests.assert_actions_result( tainted_kmods_action, level="ERROR", id="TAINTED_KMODS_DETECTED", - message="Tainted kernel modules detected:\n system76_io\n", + title="Tainted kernel modules detected", + description="Please refer to the diagnosis for further information", + diagnosis="Tainted kernel modules detected:\n system76_io\n", + remediation=( + "Prevent the modules from loading by following {0}" + " and run convert2rhel again to continue with the conversion.".format( + tainted_kmods.LINK_PREVENT_KMODS_FROM_LOADING + ) + ), ) - - else: - tainted_kmods_action.run() diff --git a/convert2rhel/unit_tests/pkghandler_test.py b/convert2rhel/unit_tests/pkghandler_test.py index 20e12cd16..d405625ce 100644 --- a/convert2rhel/unit_tests/pkghandler_test.py +++ b/convert2rhel/unit_tests/pkghandler_test.py @@ -154,17 +154,6 @@ def __call__(self, cmd, print_cmd=True, print_output=True): return self.output, self.ret_code -class GetInstalledPkgsWFingerprintsMocked(unit_tests.MockFunction): - obj1 = create_pkg_information(name="pkg1", fingerprint="199e2f91fd431d51") # RHEL - obj2 = create_pkg_information(name="pkg2", fingerprint="72f97b74ec551f03") # OL - obj3 = create_pkg_information( - name="gpg-pubkey", version="1.0.0", release="1", arch="x86_64", fingerprint="199e2f91fd431d51" # RHEL - ) - - def __call__(self, *args, **kwargs): - return [self.obj1, self.obj2, self.obj3] - - class PrintPkgInfoMocked(unit_tests.MockFunction): def __init__(self): self.called = 0 @@ -1684,7 +1673,7 @@ def test_get_installed_pkgs_by_fingerprint_incorrect_fingerprint(pretend_os, mon reason="No yum module detected on the system, skipping it.", ) @centos7 -def test_print_pkg_info_yum(pretend_os, monkeypatch): +def test_format_pkg_info_yum(pretend_os, monkeypatch): packages = [ create_pkg_information( packager="Oracle", @@ -1732,7 +1721,7 @@ def test_print_pkg_info_yum(pretend_os, monkeypatch): ), ) - result = pkghandler.print_pkg_info(packages) + result = pkghandler.format_pkg_info(packages) assert re.search( r"^Package\s+Vendor/Packager\s+Repository$", result, @@ -1756,7 +1745,7 @@ def test_print_pkg_info_yum(pretend_os, monkeypatch): reason="No dnf module detected on the system, skipping it.", ) @centos8 -def test_print_pkg_info_dnf(pretend_os, monkeypatch): +def test_format_pkg_info_dnf(pretend_os, monkeypatch): packages = [ create_pkg_information( packager="Oracle", @@ -1804,7 +1793,7 @@ def test_print_pkg_info_dnf(pretend_os, monkeypatch): ), ) - result = pkghandler.print_pkg_info(packages) + result = pkghandler.format_pkg_info(packages) assert re.search( r"^pkg1-0:0\.1-1\.x86_64\s+Oracle\s+anaconda$", @@ -1838,22 +1827,22 @@ def __call__(self, fingerprints, name=""): return [pkg_obj] -def test_get_packages_to_remove(monkeypatch): +def testget_packages_to_remove(monkeypatch): monkeypatch.setattr(system_info, "fingerprints_rhel", ["rhel_fingerprint"]) monkeypatch.setattr( pkghandler, "get_installed_pkgs_w_different_fingerprint", GetInstalledPkgObjectsWDiffFingerprintMocked() ) - original_func = pkghandler._get_packages_to_remove.__wrapped__ - monkeypatch.setattr(pkghandler, "_get_packages_to_remove", mock_decorator(original_func)) + original_func = pkghandler.get_packages_to_remove.__wrapped__ + monkeypatch.setattr(pkghandler, "get_packages_to_remove", mock_decorator(original_func)) - result = pkghandler._get_packages_to_remove(["installed_pkg", "not_installed_pkg"]) + result = pkghandler.get_packages_to_remove(["installed_pkg", "not_installed_pkg"]) assert len(result) == 1 assert result[0].nevra.name == "installed_pkg" def test_remove_pkgs_with_confirm(monkeypatch): monkeypatch.setattr(utils, "ask_to_continue", DumbCallableObject()) - monkeypatch.setattr(pkghandler, "print_pkg_info", DumbCallable()) + monkeypatch.setattr(pkghandler, "format_pkg_info", DumbCallable()) monkeypatch.setattr(pkghandler, "remove_pkgs", RemovePkgsMocked()) pkghandler.remove_pkgs_unless_from_redhat( @@ -1951,23 +1940,23 @@ def test_get_pkg_nevra(pkgmanager_name, package, include_zero_epoch, expected, m ) def test_get_third_party_pkgs(fingerprint_orig_os, expected_count, expected_pkgs, monkeypatch): monkeypatch.setattr(utils, "ask_to_continue", DumbCallableObject()) - monkeypatch.setattr(pkghandler, "print_pkg_info", PrintPkgInfoMocked()) + monkeypatch.setattr(pkghandler, "format_pkg_info", PrintPkgInfoMocked()) monkeypatch.setattr(system_info, "fingerprints_orig_os", fingerprint_orig_os) - monkeypatch.setattr(pkghandler, "get_installed_pkg_information", GetInstalledPkgsWFingerprintsMocked()) + monkeypatch.setattr(pkghandler, "get_installed_pkg_information", unit_tests.GetInstalledPkgsWFingerprintsMocked()) pkgs = pkghandler.get_third_party_pkgs() - assert pkghandler.print_pkg_info.called == expected_count + assert pkghandler.format_pkg_info.called == expected_count assert len(pkgs) == expected_pkgs def test_list_non_red_hat_pkgs_left(monkeypatch): - monkeypatch.setattr(pkghandler, "print_pkg_info", PrintPkgInfoMocked()) - monkeypatch.setattr(pkghandler, "get_installed_pkg_information", GetInstalledPkgsWFingerprintsMocked()) + monkeypatch.setattr(pkghandler, "format_pkg_info", PrintPkgInfoMocked()) + monkeypatch.setattr(pkghandler, "get_installed_pkg_information", unit_tests.GetInstalledPkgsWFingerprintsMocked()) pkghandler.list_non_red_hat_pkgs_left() - assert len(pkghandler.print_pkg_info.pkgs) == 1 - assert pkghandler.print_pkg_info.pkgs[0].nevra.name == "pkg2" + assert len(pkghandler.format_pkg_info.pkgs) == 1 + assert pkghandler.format_pkg_info.pkgs[0].nevra.name == "pkg2" @centos7 @@ -2029,7 +2018,7 @@ def test_remove_non_rhel_kernels(monkeypatch): monkeypatch.setattr( pkghandler, "get_installed_pkgs_w_different_fingerprint", GetInstalledPkgsWDifferentFingerprintMocked() ) - monkeypatch.setattr(pkghandler, "print_pkg_info", DumbCallableObject()) + monkeypatch.setattr(pkghandler, "format_pkg_info", DumbCallableObject()) monkeypatch.setattr(pkghandler, "remove_pkgs", RemovePkgsMocked()) removed_pkgs = pkghandler.remove_non_rhel_kernels() @@ -2049,7 +2038,7 @@ def test_install_additional_rhel_kernel_pkgs(monkeypatch): monkeypatch.setattr( pkghandler, "get_installed_pkgs_w_different_fingerprint", GetInstalledPkgsWDifferentFingerprintMocked() ) - monkeypatch.setattr(pkghandler, "print_pkg_info", DumbCallableObject()) + monkeypatch.setattr(pkghandler, "format_pkg_info", DumbCallableObject()) monkeypatch.setattr(pkghandler, "remove_pkgs", RemovePkgsMocked()) monkeypatch.setattr(pkghandler, "call_yum_cmd", CallYumCmdMocked()) diff --git a/convert2rhel/unit_tests/subscription_test.py b/convert2rhel/unit_tests/subscription_test.py index c115660e3..20f35089f 100644 --- a/convert2rhel/unit_tests/subscription_test.py +++ b/convert2rhel/unit_tests/subscription_test.py @@ -100,39 +100,6 @@ def __call__(self, *args, **kwargs): return return_value -class TestCheckNeededReposAvailability(object): - def test_check_needed_repos_availability(self, monkeypatch, caplog): - monkeypatch.setattr(subscription, "get_avail_repos", lambda: ["rhel_x", "rhel_y"]) - - avail_repos_message = "Needed RHEL repositories are available." - subscription.check_needed_repos_availability(["rhel_x"]) - - assert avail_repos_message in caplog.records[-1].message - - no_avail_repos_message = ( - "Some repositories are not available: rhel_z." - " Some packages may not be replaced with their corresponding" - " RHEL packages when converting. The converted system will end up" - " with a mixture of packages from RHEL and your current distribution." - ) - - subscription.check_needed_repos_availability(["rhel_z"]) - assert no_avail_repos_message in caplog.records[-1].message - - def test_check_needed_repos_availability_no_repo_available(self, monkeypatch, caplog): - monkeypatch.setattr(subscription, "get_avail_repos", lambda: []) - - no_avail_repos_message = ( - "Some repositories are not available: rhel." - " Some packages may not be replaced with their corresponding" - " RHEL packages when converting. The converted system will end up" - " with a mixture of packages from RHEL and your current distribution." - ) - subscription.check_needed_repos_availability(["rhel"]) - - assert no_avail_repos_message in caplog.records[-1].message - - class TestSubscription(unittest.TestCase): class IsFileMocked(unit_tests.MockFunction): def __init__(self, is_file): @@ -207,7 +174,7 @@ def __call__(self, msg): self.msg += "%s\n" % msg @unit_tests.mock(pkghandler, "get_installed_pkg_objects", lambda _: [namedtuple("Pkg", ["name"])("submgr")]) - @unit_tests.mock(pkghandler, "print_pkg_info", lambda x: None) + @unit_tests.mock(pkghandler, "format_pkg_info", lambda x: None) @unit_tests.mock(utils, "ask_to_continue", PromptUserMocked()) @unit_tests.mock(backup, "remove_pkgs", DumbCallable()) def test_remove_original_subscription_manager(self): @@ -222,7 +189,7 @@ def test_remove_original_subscription_manager(self): ) @unit_tests.mock(system_info, "version", namedtuple("Version", ["major", "minor"])(8, 5)) @unit_tests.mock(system_info, "id", "centos") - @unit_tests.mock(pkghandler, "print_pkg_info", lambda x: None) + @unit_tests.mock(pkghandler, "format_pkg_info", lambda x: None) @unit_tests.mock(utils, "ask_to_continue", PromptUserMocked()) @unit_tests.mock(backup, "remove_pkgs", DumbCallable()) def test_remove_original_subscription_manager_missing_package_ol_85(self): diff --git a/tests/integration/tier0/non-destructive/assessment-report/test_assessment_report.py b/tests/integration/tier0/non-destructive/assessment-report/test_assessment_report.py index a76a76725..07e581425 100644 --- a/tests/integration/tier0/non-destructive/assessment-report/test_assessment_report.py +++ b/tests/integration/tier0/non-destructive/assessment-report/test_assessment_report.py @@ -28,14 +28,13 @@ def test_failures_and_skips_in_report(convert2rhel): # Error header first assert c2r.expect("Must fix before conversion", timeout=600) == 0 - c2r.expect("SUBSCRIBE_SYSTEM::UNKNOWN_ERROR - Unable to register the system") + c2r.expect("SUBSCRIBE_SYSTEM::UNKNOWN_ERROR - Unknown error") + c2r.expect("Unable to register the system through subscription-manager.") # Skip header assert c2r.expect("Could not be checked due to other failures", timeout=600) == 0 c2r.expect("ENSURE_KERNEL_MODULES_COMPATIBILITY::SKIP - Skipped") - - # Success header - assert c2r.expect("No changes needed", timeout=600) == 0 + c2r.expect("Skipped because SUBSCRIBE_SYSTEM was not successful") assert c2r.exitstatus == 0 diff --git a/tests/integration/tier0/non-destructive/basic-sanity-checks/test_basic_sanity_checks.py b/tests/integration/tier0/non-destructive/basic-sanity-checks/test_basic_sanity_checks.py index bd182e377..f0ad3bfab 100644 --- a/tests/integration/tier0/non-destructive/basic-sanity-checks/test_basic_sanity_checks.py +++ b/tests/integration/tier0/non-destructive/basic-sanity-checks/test_basic_sanity_checks.py @@ -104,7 +104,7 @@ def test_c2r_latest_newer(convert2rhel, c2r_version, version): c2r.expect("Continue with the system conversion?") c2r.sendline("y") - assert c2r.expect("Latest available Convert2RHEL version is installed.", timeout=300) == 0 + assert c2r.expect("Latest available convert2rhel version is installed.", timeout=300) == 0 c2r.sendcontrol("c") @@ -125,7 +125,14 @@ def test_c2r_latest_older_inhibit(convert2rhel, c2r_version, version): c2r.expect("Continue with the system conversion?") c2r.sendline("y") - assert c2r.expect("CONVERT2RHEL_LATEST_VERSION::OUT_OF_DATE - You are currently running 0.01", timeout=300) == 0 + assert ( + c2r.expect( + "CONVERT2RHEL_LATEST_VERSION::OUT_OF_DATE - Outdated convert2rhel version detected", + timeout=300, + ) + == 0 + ) + assert c2r.expect("Diagnosis: You are currently running 0.01.0", timeout=300) == 0 assert c2r.expect("Only the latest version is supported for conversion.", timeout=300) == 0 c2r.sendcontrol("c") @@ -303,15 +310,10 @@ def test_disable_data_collection(shell, convert2rhel): @pytest.fixture def analyze_incomplete_rollback_envar(): os.environ["CONVERT2RHEL_UNSUPPORTED_INCOMPLETE_ROLLBACK"] = "1" - os.environ["CONVERT2RHEL_EXPERIMENTAL_ANALYSIS"] = "1" yield del os.environ["CONVERT2RHEL_UNSUPPORTED_INCOMPLETE_ROLLBACK"] - # Remove the `analyze` switch in case it won't get deleted in the test, - # so it won't interfere with other tests - if os.environ.get("CONVERT2RHEL_EXPERIMENTAL_ANALYSIS"): - del os.environ["CONVERT2RHEL_EXPERIMENTAL_ANALYSIS"] @pytest.mark.test_analyze_incomplete_rollback @@ -331,7 +333,7 @@ def test_analyze_incomplete_rollback(repositories, convert2rhel, analyze_incompl with convert2rhel("analyze --debug --no-rpm-va") as c2r: # We need to get past the data collection acknowledgement c2r.sendline("y") - c2r.expect("REMOVE_REPOSITORY_FILES_PACKAGES::PACKAGE_REMOVAL_FAILED", timeout=300) + c2r.expect("REMOVE_REPOSITORY_FILES_PACKAGES::REPOSITORY_FILE_PACKAGE_REMOVAL_FAILED", timeout=300) # Verify the user is informed to not use the envar during the analysis assert ( c2r.expect( @@ -343,8 +345,6 @@ def test_analyze_incomplete_rollback(repositories, convert2rhel, analyze_incompl # The conversion should fail assert c2r.exitstatus != 0 - del os.environ["CONVERT2RHEL_EXPERIMENTAL_ANALYSIS"] - with convert2rhel("--debug --no-rpm-va") as c2r: # We need to get past the data collection acknowledgement c2r.sendline("y") diff --git a/tests/integration/tier0/non-destructive/custom-kernel/test_custom_kernel.py b/tests/integration/tier0/non-destructive/custom-kernel/test_custom_kernel.py index b072ebb34..6d375df02 100644 --- a/tests/integration/tier0/non-destructive/custom-kernel/test_custom_kernel.py +++ b/tests/integration/tier0/non-destructive/custom-kernel/test_custom_kernel.py @@ -88,9 +88,7 @@ def test_custom_kernel(convert2rhel, shell): c2r.sendline("y") c2r.expect("WARNING - Custom kernel detected. The booted kernel needs to be signed by {}".format(os_vendor)) - c2r.expect( - "RHEL_COMPATIBLE_KERNEL::BOOTED_KERNEL_INCOMPATIBLE - The booted kernel version is incompatible with the standard RHEL kernel." - ) + c2r.expect("RHEL_COMPATIBLE_KERNEL::INVALID_KERNEL_PACKAGE_SIGNATURE") c2r.sendcontrol("c") diff --git a/tests/integration/tier0/non-destructive/custom-repository/test_custom_repository.py b/tests/integration/tier0/non-destructive/custom-repository/test_custom_repository.py index 22cc92267..0f0811249 100644 --- a/tests/integration/tier0/non-destructive/custom-repository/test_custom_repository.py +++ b/tests/integration/tier0/non-destructive/custom-repository/test_custom_repository.py @@ -109,10 +109,7 @@ def test_bad_conversion_without_rhsm(shell, convert2rhel): with convert2rhel( "-y --no-rpm-va --no-rhsm --enablerepo fake-rhel-8-for-x86_64-baseos-rpms --debug", unregister=True ) as c2r: - c2r.expect( - "CUSTOM_REPOSITORIES_ARE_VALID::UNABLE_TO_ACCESS_REPOSITORIES - Unable to access the repositories passed through the --enablerepo option. " - "For more details, see YUM/DNF output" - ) + c2r.expect("CUSTOM_REPOSITORIES_ARE_VALID::UNABLE_TO_ACCESS_REPOSITORIES") assert c2r.exitstatus == 1 diff --git a/tests/integration/tier0/non-destructive/kernel-modules/test_unsupported_kmod.py b/tests/integration/tier0/non-destructive/kernel-modules/test_unsupported_kmod.py index b5161696f..e43348521 100644 --- a/tests/integration/tier0/non-destructive/kernel-modules/test_unsupported_kmod.py +++ b/tests/integration/tier0/non-destructive/kernel-modules/test_unsupported_kmod.py @@ -74,9 +74,7 @@ def test_inhibit_if_custom_module_loaded(kmod_in_different_directory, convert2rh ), unregister=True, ) as c2r: - c2r.expect( - "ENSURE_KERNEL_MODULES_COMPATIBILITY::UNSUPPORTED_KERNEL_MODULES - The following loaded kernel modules are not available in RHEL" - ) + c2r.expect("ENSURE_KERNEL_MODULES_COMPATIBILITY::UNSUPPORTED_KERNEL_MODULES") assert c2r.exitstatus != 0 diff --git a/tests/integration/tier0/non-destructive/oracle-linux-unbreakable-enterprise-kernel/test_oracle_unsupported_uek.py b/tests/integration/tier0/non-destructive/oracle-linux-unbreakable-enterprise-kernel/test_oracle_unsupported_uek.py index bcf4a7751..a7eac871f 100644 --- a/tests/integration/tier0/non-destructive/oracle-linux-unbreakable-enterprise-kernel/test_oracle_unsupported_uek.py +++ b/tests/integration/tier0/non-destructive/oracle-linux-unbreakable-enterprise-kernel/test_oracle_unsupported_uek.py @@ -37,7 +37,7 @@ def test_bad_conversion(shell, convert2rhel): elif os.environ["TMT_REBOOT_COUNT"] == "1": with convert2rhel("-y --no-rpm-va --debug", unregister=True) as c2r: c2r.expect( - "RHEL_COMPATIBLE_KERNEL::BOOTED_KERNEL_INCOMPATIBLE - The booted kernel version is incompatible with the standard RHEL kernel", + "RHEL_COMPATIBLE_KERNEL::INCOMPATIBLE_VERSION - Incompatible booted kernel version", timeout=600, ) c2r.sendcontrol("c") diff --git a/tests/integration/tier0/non-destructive/single-yum-transaction-validation/test_single_yum_transaction_validation.py b/tests/integration/tier0/non-destructive/single-yum-transaction-validation/test_single_yum_transaction_validation.py index 051af61dc..77a3e61c1 100644 --- a/tests/integration/tier0/non-destructive/single-yum-transaction-validation/test_single_yum_transaction_validation.py +++ b/tests/integration/tier0/non-destructive/single-yum-transaction-validation/test_single_yum_transaction_validation.py @@ -11,14 +11,14 @@ SERVER_SUB = "CentOS Linux" PKGMANAGER = "yum" -FINAL_MESSAGE = "VALIDATE_PACKAGE_MANAGER_TRANSACTION::UNKNOWN_ERROR - There are no suitable mirrors available for the loaded repositories." +FINAL_MESSAGE = "Diagnosis: There are no suitable mirrors available for the loaded repositories." if "oracle" in SYSTEM_RELEASE_ENV: SERVER_SUB = "Oracle Linux Server" if "8" in SYSTEM_RELEASE_ENV: PKGMANAGER = "dnf" - FINAL_MESSAGE = "VALIDATE_PACKAGE_MANAGER_TRANSACTION::UNKNOWN_ERROR - Failed to download the transaction packages." + FINAL_MESSAGE = "Diagnosis: Failed to download the transaction packages." @pytest.fixture() @@ -86,7 +86,8 @@ def test_package_download_error(convert2rhel, shell, yum_cache): remove_entitlement_certs() - assert c2r.expect_exact(FINAL_MESSAGE, timeout=600) == 0 + assert c2r.expect("VALIDATE_PACKAGE_MANAGER_TRANSACTION::UNKNOWN_ERROR") == 0 + assert c2r.expect(FINAL_MESSAGE, timeout=600) == 0 assert c2r.exitstatus == 1 @@ -121,11 +122,12 @@ def test_transaction_validation_error(convert2rhel, shell, yum_cache): remove_entitlement_certs() assert ( c2r.expect_exact( - "VALIDATE_PACKAGE_MANAGER_TRANSACTION::UNKNOWN_ERROR - Failed to validate the yum transaction.", + "VALIDATE_PACKAGE_MANAGER_TRANSACTION::UNKNOWN_ERROR - Unknown", timeout=600, ) == 0 ) + assert c2r.expect("Failed to validate the yum transaction.", timeout=600) == 0 assert c2r.exitstatus == 1 @@ -137,7 +139,7 @@ def packages_with_period(shell): Install problematic packages with period in name. E.g. python3.11-3.11.2-2.el8.x86_64 java-1.8.0-openjdk-headless-1.8.0.372.b07-4.el8.x86_64 """ - problematic_packages = ["python3.11-3.11.2-2.el8.x86_64", "java-1.8.0-openjdk-headless-1.8.0.372.b07-4.el8.x86_64"] + problematic_packages = ["python3.11", "java-1.8.0-openjdk-headless"] # We don't care for the telemetry, disable the collection to skip over the acknowledgement os.environ["CONVERT2RHEL_DISABLE_TELEMETRY"] = "1" @@ -175,17 +177,9 @@ def test_validation_packages_with_in_name_period(shell, convert2rhel, packages_w env.str("RHSM_POOL"), ) ) as c2r: - c2r_expect_index = c2r.expect( - [ - "No problems detected during the analysis!", - "VALIDATE_PACKAGE_MANAGER_TRANSACTION::UNEXPECTED_ERROR - Unhandled exception was caught: too many values to unpack (expected 2)", - ] - ) - - if c2r_expect_index == 0: - c2r.expect("Continue with the system conversion") - c2r.sendline("n") - elif c2r_expect_index == 1: - assert AssertionError + assert c2r.expect("VALIDATE_PACKAGE_MANAGER_TRANSACTION has succeeded") == 0 + # Exit at PONR + c2r.expect("Continue with the system conversion?") + c2r.sendline("n") assert c2r.exitstatus != 0