Skip to content

Action Framework

Toshio Kuratomi edited this page Oct 5, 2023 · 10 revisions

Action Framework

WARNING: This feature is currently in a Technical Preview.

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.

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()

(Toshio)

Action.add_message()

(Toshio)

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 report in the summary will always be sorted in order of severity, so the list will always go like:

  • ERROR
  • OVERRIDABLE
  • SKIP
  • WARNING
  • INFO
  • SUCCESS

Exceptions in Actions

Action framework catch-all

(Toshio)

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, find where logger.critical() is being called from the Action (usually, several calls deep into convrt2rhel's non-Action code.) Then replace the logger.critical() call with logger.critical_no_exit() and 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 will handle CriticalError, log the exception using logger.critical_no_exit() and then perform the standard exit procedures (rollback if necessary, and etc). The exception handler will use CriticalError.diagnosis as the message for logger.critical_no_exit(). So consider that when populating the diagnosis field.