Skip to content

Actions

SandakovMM edited this page Jun 1, 2023 · 1 revision

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

Description

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.

Example

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

How to Add to the Check Flow

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.

Check Flow

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 Action

Description

Active actions are responsible for performing changes on a system. They can have three types of subactions:

  1. Preparation - This subaction is executed during the preparation stage before the actual conversion process begins. To implement it, use the _prepare_action method.
  2. 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.
  3. 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.

Example

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

How to Add an ActiveAction to the Actions Flow

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(),
            #...
        ],
    #...

Active Action Flows

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 and RevertActionsFlow. 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.

Unit Tests

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.