Skip to content

Action Framework

Freya Gustavsson edited this page Aug 7, 2024 · 10 revisions

Action Framework

The Action Framework allows Convert2RHEL to execute a pre conversion analysis that will let the user identify if the conversion will be successful up to the point of the return.

Action Classes

Action classes are a way to implement checks that needs to run during the conversion to identify any problems with the system before we reach to a critical part of the conversion called Point of no Return.

Action and Message IDs

Format and naming

Action IDs

Each action has unique IDs to describe the class

  • Action ID MUST BE written with current tense.
  • Action ID SHOULD begin with the action being taken
Examples
# ID examples for an action class
id = "LIST_THIRD_PARTY_PACKAGES"    # GOOD
id = "LISTED_THIRD_PARTY_PACKAGES"  # BAD

id = "BACKUP_REDHAT_RELEASE"        # GOOD
id = "REDHAT_RELEASE_BACKUP"        # DISCOURAGED

Message IDs

Action CAN set a message for the class. Every message needs an ID

  • Message ID MUST BE written with past tense.
  • Message ID MUST BE unique between different Action
  • Message ID SHOULD begin with the action that was taken
  • Message ID CAN BE unique within the Action
Examples
# ID examples used within self.add_message()
id = "SKIPPED_MODIFIED_RPM_FILES_DIFF"   # GOOD
id = "SKIP_MODIFIED_RPM_FILES_DIFF"      # BAD

id = "FOUND_MODIFIED_RPM_FILES"          # GOOD
id = "MODIFIED_RPM_FILES_FOUND"          # DISCOURAGED

Examples

Normal action class for a regular check that does not depend on anything else and will execute flawlessly.

__metaclass__ = type

from convert2rhel import actions 
import logging

logger = logging.getLogger(__name__)

class YourAction(actions.Action):
    # `id` is always required for the Action.
    id = "YOUR_ACTION"

    # `dependencies` is optional. If you aciton doesn't have any dependency, 
    # you don't need that field to be present.
    dependencies = ()

    def run(self):
        super(YourAction, self).run()

        logger.task("Performing very important task!")
        perform_your_very_important_task()

Action class that set a different result after it's execution. Usually, the self.set_result() function is used to set error results after the execution.

__metaclass__ = type

from convert2rhel import actions 
import logging

logger = logging.getLogger(__name__)

class YourAction(actions.Action):
    id = "YOUR_ACTION"

    def run(self):
        super(YourAction, self).run()

        logger.task("Performing a task that will fail :(")
        has_failed = a_task_that_will_fail()

        if has_failed:
            # In case of failure, you need to set the result 
            self.set_result(status="ERROR", error_id="SOME_ERROR", message="It failed :(")
        else:
            self.set_result(status="WARNING", error_id="SOME_WARNING", message="It succeed, but with some warnings.")

Below, an example has actions with dependencies

__metaclass__ = type

from convert2rhel import actions 
import logging

logger = logging.getLogger(__name__)

class YourDependencyAction(actions.Action)
    id = "NICE_DEPENDENCY"

    def run(self):
        super(YourDependencyAction, self).run()
        
        logger.info("This action class will run first")
        perform_some_dependency_thing()

class YourAction(actions.Action):
    id = ""
    dependencies = (
        "NICE_DEPENDENCY",
        # Or, you could use, which ideally is the same as the above.
        YourDependencyAction.id,    
    )

    def run(self):
        super(YourAction, self).run()

        logger.info("This action will run if `YourDependencyAction` succeed.")
        now_we_execute_this_action()

Regarding the above examples, whenever we have dependencies in an action class, the dependency will run first, and if the dependency succeeds, your action will ran and execute it's own code, otherwise, if the dependency failed, an SKIP status will be set for the dependency, and any dependant action will not execute.

A note on dependencies

Dependencies are normal actions that are supposed to ensure that specific code will run before your main action. Usually, dependencies are used to perform setups, cleanups, or any other action that needs to prepare a specific behavior before an determinated action runs.

Another way of using dependencies, is to ensure correct ordering of action runs. By default, the Action Framework will run actions in different orders if they don't have any dependencies, making this a bit harder to ensure that the actions will always run and keep the same order of execution.

In order to overcome this situation, actions can depend on each other to ensure that they will run only after the successful state of the dependent action.

Down below, you can see a very minimalistic example of how the actions structure are supposed to be.

Actions folder structure

actions/
├── __init__.py                     # The main module where all actions related functions and classes are stored.
├── pre_ponr_changes                # Module that keep together all pre point of no return actions.
│   ├── handle_packages.py
│   ├── __init__.py
│   ├── kernel_modules.py
│   ├── special_cases.py
│   ├── subscription.py
│   └── transaction.py
├── report.py                       # The report module where wwe output a summary to the user regarding the action runs.
└── system_checks                   # Module that keep together all system checks actions.
    ├── convert2rhel_latest.py
    ├── custom_repos_are_valid.py
    ├── dbus.py
    ├── efi.py
    ├── __init__.py
    ├── is_loaded_kernel_latest.py
    ├── package_updates.py
    ├── readonly_mounts.py
    ├── rhel_compatible_kernel.py
    └── tainted_kmods.py

3 directories, 22 files

Returning messages for the report

Action.set_result()

When returning from an Action, there must be a result set. By default, Actions have a default of SUCCESS set. This result has no useful information other then the status of SUCCESS.

If there are any errors, you need to call Action.set_result() to return an error as the result instead of SUCCESS. As of this writing, ERROR, OVERRIDABLE, SKIP, and SUCCESS are the valid levels for a result.

In almost all cases, you should return after calling Action.set_result().

Action.add_message()

Sometimes an Action discovers something about a system that doesn't prevent the conversion from proceeding but it would be useful for the user to be informed of.

At the present time, WARNING and INFO are the levels that are valid for a message.

See Also

Report

The Action Framework counts with a report feature, which by the end of the execution of all actions, will output a summary to the user regarding if the failure/warnings of the actions executed. This report is especially useful for the users that want to do a pre analysis conversion before running the tool, allowing them to see, beforehand, if we have detected any problems which might prevent the conversion from succeeding.

The summary that we output is very simple, it consists of reporting about tasks that either failed, had an warning, were skipped because of a dependency failure, or failed but allow the user to override the check if they run convert2rhel again.It is good to note that the report described above is only when the user is doing a normal conversion. In case that the user is doing a pre analysis conversion, then we gonna output all logs in the report, as more information may be needed before progressing with the conversion.

Example of report messages in case of some actions not completing successfully:

[03/31/2023 17:43:52] TASK - [Prepare: Conversion analysis report] **********************************

(ERROR) ErrorAction.ERROR: ERROR MESSAGE
(OVERRIDABLE) OverridableAction.OVERRIDABLE: OVERRIDABLE MESSAGE
(WARNING) WarningAction.WARNING: WARNING MESSAGE

Example of report in case of all actions succeeding and no report is needed to be presented

[03/31/2023 17:43:52] TASK - [Prepare: Conversion analysis report] **********************************

No problems detected during the analysis!

The messages that are reported in the summary will always be sorted in order of severity, so the list will always go like:

  • ERROR: A problem that must be fixed before the system can be converted.
  • OVERRIDABLE: A problem that the user can fix or tell convret2rhel to ignore when they run it the next time (usually via an environment variable).
  • SKIP: An Action which was skipped due to an Action which it depended on failing.
  • WARNING: A message which says that something the user should look into and possibly fix exists but the conversion can proceed despite its presence.
  • INFO: A message which could be helpful to the user but doesn't indicate a problem.
  • SUCCESS: The Action completed successfully.

Exceptions in Actions

Action framework catch-all

When the Action framework executes each Action plugin, it runs them inside a catch-all try-except block. This block catches any type of exception that an Action plugin can raise (except (Exception, SystemExit)) so that the Action plugins cannot cause the convret2rhel process to exit in the middle of running the Actions. Any unhandled exceptions caught here are saved into the report as UNEXPECTED_ERRORS. They should be treated as bugs and either fixed in the relevant Action or caught in that Action and set as that Action's result with a user-friendly id, title, description, and diagnosis.

Actions and SystemExit/sys.exit

When we first created the Action framework for the Pre-conversion Assessment, we needed to do it quickly. In the interest of development speed we left a lot of places where we call logger.critical() in the code called by the Action plugins. The problem with that is that the current logger.critical() calls sys.exit() to exit convert2rhel.

We've worked around this in various ways (See also the catch-all section above) but another problem is that logger.critical() + sys.exit() only gives us one short line of text to describe what went wrong. Now that we're outputting the report to Insights, we need to make sure we return more information from each Action to the Action framework. That way the report will display complete, user-oriented information (including at least title, id, and diagnosis). We are still working under the pressure of a deadline, though, so the port away from logger.critical() uses a special Exception, exceptions.CriticalError which contains all the information that the assessment report needs. (We should eventually move to a model where we raise a custom exception in the library code which the Action code can use to decide what to print. That way the caller will be making the decisions as to what should happen rather than the

To port:

  1. Find where logger.critical() is being called from the Action (usually, several calls deep into convrt2rhel's non-Action code.)
  2. Replace the logger.critical() call with logger.critical_no_exit()
  3. Explicitly raise CriticalError. CriticalError takes several arguments which mirror the fields that an Action will return as a result. Fill those in with values that will make sense to the readers of the report.

Note that if the function is called from outside of Action code, it will now raise CriticalError in a location that isn't prepared to catch it. In general, this is probably okay as that location probably didn't catch the SystemExit() which logger.critical() raised before the port. There is a new exception handler in main.main_locked() which handles CriticalError, logs the exception using logger.critical_no_exit() and then performs the standard exit procedures (rollback if necessary, etc). The exception handler uses CriticalError.diagnosis as the message for logger.critical_no_exit(). So put enough information into diagnosis to make it useful.