-
Notifications
You must be signed in to change notification settings - Fork 11
Actions
There are two types of actions: CheckAction
and ActiveAction
.
Ideally, all actions should:
- Be as granular as possible
- Be tied to one specific aspect of the system
- Be safely callable multiple times
All actions should be placed in a file inside the actions subdirectory, except for action.py, which describes abstract classes. If none of the specific action files fit your requirements, you can use common.py for ActiveActions
or common_checks.py for CheckActions
. Alternatively, you can create your own file.
Check actions are actions that verify specific aspects of the system. They could be used to prevent the conversion if the check is failed. The caller invokes the do_check
method to perform the check. When everything is in order, do_check
should return True
. If there is an issue that requires preventing the conversion, do_check
should return False
.
To create your own check action, create a class that inherits from CheckAction
. Redefine the _do_check
method and set the name
and description
attributes in the __init__
method:
-
self.name
is a concise string that will be displayed as the header of the error message. -
self.description
is a detailed description of the problem that will be shown after the error message header. Ideally, it should include instructions on how to resolve the problem. Additionally, the description can be modified during the_do_check
method if you need to specify which items/files/etc triggered the check.
class MyCheck(CheckAction):
def __init__(self):
self.name = "my check" # Header of the error message
self.description = "There is a problem that you can fix with this action" # Detailed description of the error
def _do_check(self):
if not os.path.exists("/my/important/file"):
self.description += "\n/my/important/file is missing" # You can modify the description during the check
return False # Return False if the check fails
return True # Return True if the check passes
To add a new check action to the check flow, add it to the checks list in the is_required_conditions_satisfied function in main.py.
The check flow invokes all check actions, aggregates the results from each check, and returns a list of messages from failed checks. This allows us to display all the failed checks simultaneously if there are multiple failures.
The check flow is implemented in the CheckFlow
class and is called within the is_required_conditions_satisfied
function in main.py
.
Active actions are responsible for performing changes on a system. They can have three types of subactions:
- Preparation - This subaction is executed during the preparation stage before the actual conversion process begins. To implement it, use the
_prepare_action
method. - Finish - This subaction is executed after the conversion process is completed. It is used for preparing and starting Plesk when AlmaLinux is ready. To implement it, use the
_post_action
method. - Revert - This subaction is executed during the revert process. It is used to restore Plesk to a working state if the preparation process fails. To implement it, use the
_revert_action
method.
An active action can implement any combination of these methods. Note that all of these methods should be redefined, or you will encounter exceptions about unimplemented methods. If you don't need to perform any action during a particular stage, simply use the pass
statement. The exceptions are intentionally added to ensure that the action creator doesn't forget to include the required actions.
Active actions also support skipping. For example, if a component or package is not installed, you can skip its reinstallation. To determine if an action should be skipped, implement the _is_required
method. If the action should be skipped, the method should return False
; otherwise, it should return True
. Note that we shouldn't check if the action has already been performed within the _is_required
method because the finish stage will be skipped for all skipped actions. If you want to avoid performing the same action twice, do the checks within the _prepare_action
method rather than the _is_required
method.
Each action can have an estimated performing time, which is used to show progress in the progress bar. Redefine the estimate_prepare_time
, estimate_post_time
, and estimate_revert_time
methods based on your expectations for the performing time to ensure that the progress bar works correctly. Usually, we allocate slightly more time to take into account long perform on older machines. By default, the estimate functions return 1 second.
In the __init__
method of the action, you should always redefine self.name
and self.description
:
-
self.name
is a string that will be shown in the progress bar. Note that it should not exceed 40 characters. -
self.description
is a string that provides additional information about the action and is shown in all logs beneath the action's status.
Creating action example:
class MyActiveAction(ActiveAction):
def __init__(self):
self.name = "my active action" # The string will be shown in progress bar
self.description = "Make my changes" # The string will be shown in logs
self.data_path = "/etc/mydaemon/myfile" # Just to make example more relevant
def _is_required(self):
if not os.path.exists(self.data_path):
return False # return False to skip the action
return True # return True to perform the action
def _prepare_action(self): # This method will be called on prepare stage (when we at CentOS 7)
with open(self.data_path, "a") as myfile:
myfile.write("Something important")
def _post_action(self): # This method will be called on finish stage (when we at AlmaLinux 8)
os.unlink(self.data_path)
def _revert_action(self): # This method will be called on revert stage (when we at CentOS 7 and preparation is failed)
os.unlink(self.data_path)
def estimate_prepare_time(self):
return 2 # We expect that _prepare_action will take 2 seconds to perform the action
def estimate_post_time(self):
return 2 # We expect that __post_action will take 2 seconds to perform the action
def estimate_revert_time(self):
return 2 # We expect that _revert_action will take 2 seconds to perform the action
To add an action, you need to include it in the actions_map
within the construct_actions
function. The map is divided into several steps to establish dependencies between actions. Some actions, for example, depend on the installation of Elivate because they modify the Elivate configuration. Therefore, we need to ensure that the Elivate installation is given the highest priority. Note that priority of '1' is higher than priority of '2'.
It's important to note that the priorities are reversed for the finish stage and the reverting process. This is because we want to perform actions in reverse order during the revert process. So, if you're adding your action specifically for the finish stage, be careful with the assigned priorities. The reverse order is necessary because most actions should be reverted in a specific order. For example (though this example is exaggerated), we don't want to uninstall Elivate before reverting changes in Elivate-related configurations.
Adding action example:
```python
def construct_actions(options, stage_flag):
actions_map = {}
#...
2: [ # <--- The priority
actions.LeapReposConfiguration(),
actions.AvoidMariadbDowngrade(),
action.MyActiveAction(), # <---- There is our new action
actions.LeapChoicesConfiguration(),
actions.AdoptKolabRepositories(),
#...
],
#...
The active action flows are implemented by classes inherited from ActiveFlow
: PrepareActionsFlow
, FinishActionsFlow
, and RevertActionsFlow
. The key differences between them are:
- They use different perform methods for executing actions.
- The order of actions is reversed for
FinishActionsFlow
andRevertActionsFlow
. The order of actions is defined in the_get_flow()
method.
It is generally recommended not to add new flows or modify existing ones. This information is provided for reference purposes only.
Currently, we do not perform unit tests specifically for the actions. In most cases, actions involve making specific changes to files or calling utilities without complex logic. Therefore, unit tests for actions would mainly focus on testing mocks, which may not provide much value. However, we do conduct tests for the Action
and ActionsFlow
classes, as they are the main abstractions and undergo periodic refactoring.