-
Notifications
You must be signed in to change notification settings - Fork 84
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.
- 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.
- IDs SHOULD BE in CONSTANT_CASE format, AKA screaming snake case.
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
# 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
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
# 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
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
When returning from an Action
, there must be a result set. By default, Action
s 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 level
s for a result.
In almost all cases, you should return
after calling Action.set_result()
.
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.
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
: AnAction
which was skipped due to anAction
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.
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.
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 theAction
(usually, several calls deep into convrt2rhel's non-Action code.) - Replace the
logger.critical()
call withlogger.critical_no_exit()
- Explicitly raise
CriticalError
.CriticalError
takes several arguments which mirror the fields that anAction
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.