Build a new action

Watcher Applier has an external action plugin interface which gives anyone the ability to integrate an external action in order to extend the initial set of actions Watcher provides.

This section gives some guidelines on how to implement and integrate custom actions with Watcher.

Creating a new plugin

First of all you have to extend the base BaseAction class which defines a set of abstract methods and/or properties that you will have to implement:

  • The schema is an abstract property that you have to implement. This is the first function to be called by the applier before any further processing and its role is to validate the input parameters that were provided to it.

  • The pre_condition() is called before the execution of an action. This method is a hook that can be used to perform some initializations or to make some more advanced validation on its input parameters. If you wish to block the execution based on this factor, you simply have to raise an exception.

  • The post_condition() is called after the execution of an action. As this function is called regardless of whether an action succeeded or not, this can prove itself useful to perform cleanup operations.

  • The execute() is the main component of an action. This is where you should implement the logic of your action.

  • The revert() allows you to roll back the targeted resource to its original state following a faulty execution. Indeed, this method is called by the workflow engine whenever an action raises an exception.

Here is an example showing how you can write a plugin called DummyAction:

# Filepath = <PROJECT_DIR>/thirdparty/dummy.py
# Import path = thirdparty.dummy
import voluptuous

from watcher.applier.actions import base


class DummyAction(base.BaseAction):

    @property
    def schema(self):
        return voluptuous.Schema({})

    def execute(self):
        # Does nothing
        pass  # Only returning False is considered as a failure

    def revert(self):
        # Does nothing
        pass

    def pre_condition(self):
        # No pre-checks are done here
        pass

    def post_condition(self):
        # Nothing done here
        pass

This implementation is the most basic one. So in order to get a better understanding on how to implement a more advanced action, have a look at the Migrate class.

Input validation

As you can see in the previous example, we are using Voluptuous to validate the input parameters of an action. So if you want to learn more about how to work with Voluptuous, you can have a look at their documentation:

Define configuration parameters

At this point, you have a fully functional action. However, in more complex implementation, you may want to define some configuration options so one can tune the action to its needs. To do so, you can implement the get_config_opts() class method as followed:

from oslo_config import cfg

class DummyAction(base.BaseAction):

    # [...]

    def execute(self):
        assert self.config.test_opt == 0

    @classmethod
    def get_config_opts(cls):
        return super(
            DummyAction, cls).get_config_opts() + [
            cfg.StrOpt('test_opt', help="Demo Option.", default=0),
            # Some more options ...
        ]

The configuration options defined within this class method will be included within the global watcher.conf configuration file under a section named by convention: {namespace}.{plugin_name}. In our case, the watcher.conf configuration would have to be modified as followed:

[watcher_actions.dummy]
# Option used for testing.
test_opt = test_value

Then, the configuration options you define within this method will then be injected in each instantiated object via the config parameter of the __init__() method.

Abstract Plugin Class

Here below is the abstract BaseAction class that every single action should implement:

class watcher.applier.actions.base.BaseAction(config, osc=None)[source]
schema

Defines a Schema that the input parameters shall comply to

Returns:

A schema declaring the input parameters this action should be provided along with their respective constraints (e.g. type, value range, …)

Return type:

voluptuous.Schema instance

__init__(config, osc=None)[source]

Constructor

Parameters:
  • config (dict) – A mapping containing the configuration of this action

  • osc (OpenStackClients instance, optional) – an OpenStackClients instance, defaults to None

abstract execute()[source]

Executes the main logic of the action

This method can be used to perform an action on a given set of input parameters to accomplish some type of operation. This operation may return a boolean value as a result of its execution. If False, this will be considered as an error and will then trigger the reverting of the actions.

Returns:

A flag indicating whether or not the action succeeded

Return type:

bool

classmethod get_config_opts()[source]

Defines the configuration options to be associated to this loadable

Returns:

A list of configuration options relative to this Loadable

Return type:

list of oslo_config.cfg.Opt instances

abstract get_description()[source]

Description of the action

abstract post_condition()[source]

Hook: called after the execution of an action

This function is called regardless of whether an action succeeded or not. So you can use it to perform cleanup operations.

abstract pre_condition()[source]

Hook: called before the execution of an action

This method can be used to perform some initializations or to make some more advanced validation on its input parameters. So if you wish to block its execution based on this factor, raise the related exception.

abstract revert()[source]

Revert this action

This method should rollback the resource to its initial state in the event of a faulty execution. This happens when the action raised an exception during its execute().

abstract property schema

Defines a Schema that the input parameters shall comply to

Returns:

A schema declaring the input parameters this action should be provided along with their respective constraints

Return type:

jsonschema.Schema instance

Register a new entry point

In order for the Watcher Applier to load your new action, the action must be registered as a named entry point under the watcher_actions entry point of your setup.py file. If you are using pbr, this entry point should be placed in your setup.cfg file.

The name you give to your entry point has to be unique.

Here below is how you would proceed to register DummyAction using pbr:

[entry_points]
watcher_actions =
    dummy = thirdparty.dummy:DummyAction

Using action plugins

The Watcher Applier service will automatically discover any installed plugins when it is restarted. If a Python package containing a custom plugin is installed within the same environment as Watcher, Watcher will automatically make that plugin available for use.

At this point, you can use your new action plugin in your strategy plugin if you reference it via the use of the add_action() method:

# [...]
self.solution.add_action(
    action_type="dummy",  # Name of the entry point we registered earlier
    applies_to="",
    input_parameters={})

By doing so, your action will be saved within the Watcher Database, ready to be processed by the planner for creating an action plan which can then be executed by the Watcher Applier via its workflow engine.

At the last, remember to add the action into the weights in watcher.conf, otherwise you will get an error when the action be referenced in a strategy.

Scheduling of an action plugin

Watcher provides a basic built-in planner which is only able to process the Watcher built-in actions. Therefore, you will either have to use an existing third-party planner or implement another planner that will be able to take into account your new action plugin.

Test your new action

In order to test your new action via a manual test or a Tempest test, you can use the Actuator strategy and pass it one or more actions to execute. This way, you can isolate your action to see if it works as expected.