Linchpin Custom Action Managers

Linchpin custom action managers:

In linchpin, ActionManagers are set of python interfaces responsible for execution of linchpin hook based on their type. There are two types of ActionManagers builtins and custom.

Here’s a list of built-in Action Managers:

  • shell: Allows either inline shell commands or an executable shell script

  • python: Executes a Python script

  • ansible: Executes an Ansible playbook, allowing passing of a vars_file and extra_vars represented as a Python dict

  • nodejs: Executes a Node.js script

  • ruby: Executes a Ruby script

In addition to the above action managers, User can define their custom action manager. custom/userdefined action managers are helpful when there is a specific runtime end user would like to make use of for executing a hook.

For example, if linchpin end user would like to use a “xyz” language based runtime or a custom command to be run when certain paramters are passed to a hook. They can do it with help of hook based on custom_action_manager.

Consider the following dummy workspace example for custom_action_manager:

.
├── credentials
├── hooks
│   ├── custom
│   │   └── somecustomhook
│   │       ├── custom_action_manager.py
│   │       ├── custom_action_manager.pyc
│   │       └── test_custom.py
├── inventories
├── layouts
├── linchpin.conf
├── linchpin.log
├── localhost
├── PinFile
├── resources
└── topologies
  └── dummy-topology.yml
---
dummy_target:
  topology:
    topology_name: "dummy"
    resource_groups:
    - resource_group_name: "dummy"
      resource_group_type: "dummy"
      resource_definitions:
      - role: "dummy_node"
        name: "web"
        count: 1
  layout:
    inventory_layout:
      vars:
        hostname: __IP__
      hosts:
        example-node:
          count: 1
          host_groups:
            - example
  hooks:
    postup:
      - name: somecustomhook
        type: custom
        action_manager: custom_action_manager.py
        # action_manager: /path/to/manager
        # if not absolute path
        # linchpin searches in hooks folder configured
        context: True
        actions:
          - script: some_script.go

As you can see in the above structure the custom hook follows the same structure of a userdefined hook. However, we also need to add python interface custom_action_manager.py (which can be named any) within thehooks folder or the absolute path to the python file is to be mentioned in the Pinfile

In order to write a custom_action_manager one has to implement builtin linchpin ActionManager class overriding the following functions:

  • validate: (optional): validate schema for hook designed

  • load: How to load the context parameters

  • execute: Responsible for executing the files based on the parameters

Once the above functions are implemented the class file can be included in Pinfile.

Following is an example for the python interface implemented:

import os
import yaml
import json

from cerberus import Validator

from linchpin.exceptions import HookError
from linchpin.hooks.action_managers.action_manager import ActionManager


class CustomActionManager(ActionManager):

    def __init__(self, name, action_data, target_data, **kwargs):

        """
        The following is an example for CustomActionManager
        AnsibleActionManager constructor
        :param name: Name of Action Manager , ( ie., ansible)
        :param action_data: dictionary of action_block
        consists of set of actions
        example:
        - name: nameofthehook
          type: custom
          actions:
            - script: test_playbook.yaml
        :param target_data: Target specific data defined in PinFile
        :param kwargs: anyother keyword args passed as metadata
        """

        self.name = name
        self.action_data = action_data
        self.target_data = target_data
        self.context = kwargs.get("context", True)
        self.kwargs = kwargs


    def validate(self):

        """
        Validates the action_block based on the cerberus schema
        example:: ansible_action_block::::
        - name: nameofthehook
          type: customhook
          actions:
            - script: test_playbook.yaml
        """
        """
        schema = {
            'name': {'type': 'string', 'required': True},
            'type': {'type': 'string', 'allowed': ['custom']},
            'path': {'type': 'string', 'required': False},
            'context': {'type': 'boolean', 'required': False},
            'actions': {
                'type': 'list',
                'schema': {
                    'type': 'dict',
                    'schema': {
                        'script': {'type': 'string', 'required': True}
                    }
                },
                'required': True
            }
        }

        v = Validator(schema)
        status = v.validate(self.action_data)

        if not status:
            raise HookError("Invalid syntax: {0}".format((v.errors)))
        else:
            return status


    def load(self):

        """
        Loads the ansible specific managers and loaders
        """
        return True

    def get_ctx_params(self):

        """
        Reformats the ansible specific context variables
        """

        ctx_params = {}
        ctx_params["resource_file"] = (
            self.target_data.get("resource_file", None))
        ctx_params["layout_file"] = self.target_data.get("layout_file", None)
        ctx_params["inventory_file"] = (
            self.target_data.get("inventory_file", None))

        return ctx_params


    def execute(self):

        """
        Executes the action_block in the PinFile
        The following logic just prints out path of the script being used
        """

        self.load()
        extra_vars = {}
        runners = []

        print("This is the custom hook that runs custom logic")

        for action in self.action_data["actions"]:
            path = self.action_data["path"]
            script = action.get("script")
            print(script)
            print(path)