-
Notifications
You must be signed in to change notification settings - Fork 83
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.
- Presentation giving an Overview of the Action Framework
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.
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.
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/
├── __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
(Toshio)
(Toshio)
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
(Toshio)
When we first created the Action
framework for the Pre-conversion Assessment, we needed to do it quickly. One of the things we had to do was keep a lot of places where we call logger.critical()
. One of the things the current logger.critical()
does is exit by calling sys.exit()
. This causes the Action
to exit with only a small message about 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. 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.
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.