Table of contents
1.
Introduction
2.
Developing plugins
2.1.
Writing plugins in Python
2.2.
Raising errors
2.3.
String Encoding
2.4.
Plugin configuration and Documentation standards
3.
Developing particular plugin types
3.1.
Action plugins
3.2.
Cache plugins
3.3.
Connection plugins
3.4.
Filter plugins
3.5.
Inventory Plugins
3.6.
Test plugins
3.7.
Vars plugins
4.
Frequently Asked Questions
4.1.
Which language should be used for writing plugins?
4.2.
Why should the user convert the strings returned by the plugin into Python's Unicode type?
4.3.
Mention any commonly used connection plugin.
5.
Conclusion
Last Updated: Mar 27, 2024
Medium

Ansible-Developing plugins

Career growth poll
Do you think IIT Guwahati certified course can help you in your career?

Introduction

Ansible is a simple IT automation engine that is used to automate cloud provisioning, configuration management, application deployment, and many other IT requirements. It has the capability for multi-tier deployment. It models the user's IT infrastructure by describing how all of the systems inter-relate, rather than just managing one system at a time. It is very simple to deploy as it uses no agents and requires no additional custom security infrastructure. 

Developing plugins

Plugins increase Ansible's core functionality with logic and features that are accessible to all modules. There are a number of handy plugins with which Ansible comes. Apart from that, the user can easily write their own. All the ansible plugins must have:

  • It must be written in Python.
  • Raise Errors.
  • Return strings in Unicode.
  • Conform to Ansible's configuration and documentation standards.

Writing plugins in Python

The user needs to write the plugin in Python so that it can be loaded by the PluginLoader and returned in the form of a Python object that any module can use. As the plugins are executed on the controller, therefore they must be written in a compatible version of Python.

Raising errors

The user should return errors that are encountered during plugin execution either by raising AnsibleError() or by using a similar class with a message describing the error. The user should use the to_native ansible function to ensure proper string compatibility across the Python versions whenever wrapping other exceptions into error messages:

from ansible.module_utils.common.text.converters import to_native

try:
    cause_an_exception()
except Exception as e:
    raise AnsibleError('Something happened, this was original exception: %s' % to_native(e))


Variables are only evaluated by the Ansible when they are needed, filter and test plugins should propagate the exceptions jinja2.exceptions.UndefinedError & AnsibleUndefinedVariable to make sure undefined variables are only fatal when necessary.

String Encoding

The user needs to convert any string that is returned by the plugins into Python's Unicode type. By converting them to Unicode, it is ensured that the strings can run through Jinja2. To convert strings, the following method is used:

from ansible.module_utils.common.text.converters import to_text
resultString = to_text(resultString )

Plugin configuration and Documentation standards

The user can define configurable options for its plugin by describing them in the DOCUMENTATION section of the python file. Since Ansible version 2.4, callback and connections plugins have declared configuration requirements in this manner. This ensures that the documentation of the user's plugin options will always be correct. The user can use the below-mentioned format to add a configurable option to the plugin

options:
  option_name:
    description: describe this config option
    default: default value for this config option
    env:
      - name: NAME_OF_ENV_VAR
    ini:
      - section: section_of_ansible.cfg_where_this_config_option_is_defined
        key: key_used_in_ansible.cfg
    vars:
      - name: name_of_ansible_var
      - name: name_of_second_var
        version_added: X.x
    required: True/False
    type: boolean/float/integer/list/none/path/pathlist/pathspec/string/tmppath
    version_added: X.x


The user can use the self.get_option(<option_name>) to access the configuration systems in the plugin. For plugin types which support embedded documentation, the settings are pre-populated by the controller. If the user wants to populate the settings explicitly, the user can use self.set_options() call.

A well-formed doc string must be included with the plugins that support embedded documentation. If a user inherits from a plugin, then the user must document the options it takes, either through a documentation fragment or as a copy.

Developing particular plugin types

Action plugins

It allows the user to integrate local processing as well as local data with the module functionality. A user can create an action plugin by creating a new class with the Base class as the parent:

from Ansible.plugins.action import ActionBase

class ActionModule(ActionBase):
    pass
Then from there, using the _execute_module method, start executing the module to call the original module. Once the module is successfully executed, the user can modify the module return data.
module_return = self._execute_module(module_name='<NAME_OF_MODULE>',
                                    module_args=module_args,
                                    task_vars=task_vars, tmp=tmp)

Cache plugins

They store gathered facts as well as data retrieved by the inventory plugins. To use the self.set_options() and self.get_option(<option_name>) the user needs to import cache plugins using the cache_loader. If the user imports a cache plugin directly into the code base, then the user can only access options via Ansible.constants and can break the cache plugin's ability to be used by an inventory plugin.

from ansible.plugins.loader import cache_loader
[...]
plugin = cache_loader.get('custom_cache', **cache_kwargs)


Cache plugins have two base classes: BaseCacheModule, which is for database-backed caches, and BaseCacheFileModule, which is for file-backed caches.

If the user wants to create a cache plugin, then the user needs to start by creating a new CacheModule class with an appropriate base class. If the user is creating a plugin using an _init_ method, then the user should initialize the base class with any provided args and kwargs so that it's compatible with the inventory plugin cache options. The base class makes a call to self.set_options(direct=kwargs). The self.get_option(<option_name>) should be used for accessing the cache options once the base class _init_ method has been called.

Options such as _uri, _prefix, and _timeout should be taken by new cache plugins to be consistent with the existing cache plugins.

from ansible.plugins.cache import BaseCacheModule

class CacheModule(BaseCacheModule):
    def __init__(self, *args, **kwargs):
        super(CacheModule, self).__init__(*args, **kwargs)
        self._connection = self.get_option('_uri')
        self._prefix = self.get_option('_prefix')
        self._timeout = self.get_option('_timeout')


If in case the user uses the BaseCacheModule, then the user must implement the methods get, contains, keys, set, delete, flush, and copy. A boolean value which indicates whether the key exists or not should be returned by the contains method. The get method doesn't raise a KeyError in case the cache has expired.

If in case the user uses the BaseFileCacheModule, then the user must implement _load and _dump methods as they will be called from the base class methods get and set. 

Connection plugins

It allows Ansible to connect with the target hosts allowing it to execute tasks on them. Though Ansible comes with many connection plugins, at a time, only one can be used per host. Native ssh, paramiko SSH, and local connection types are some of the most commonly used connection plugins. All of these have the ability to be used in playbooks and /usr/bin/ansible to connect to remote machines.

Since the ansible version 2.1, the smart connection plugins have been introduced. The smart connection type allows the Ansible to automatically select either paramiko or the openssh connection plugin, depending on the system's capabilities. 

If the user wants to create a new connection plugin, then copy the format of one of the existing connection plugins and just drop it into the connection directory onto the local plugin path.

Filter plugins

They are used to manipulate the data. It is a feature of Jinja2 and is also available for use in Jinja2 templates, which are used by the template module. All the plugins can be extended easily, but the user can have several per file rather than having a file for each. Almost all of the filter plugins shipped with the Ansible reside in core.py.

These plugins don't use the standard configuration and documentation system.

Ansible evaluates variables only when they are required; therefore, the filter plugins should propagate the exceptions jinja2.exceptions.UndefinedError and AnsibleUndefinedVariable to make sure that the undefined variables are only fatal when necessary. 

try:
    cause_an_exception(with_undefined_variable)
except jinja2.exceptions.UndefinedError as e:
    raise AnsibleUndefinedVariable("Something happened, this was the original exception: %s" % to_native(e))
except Exception as e:
    raise AnsibleFilterError("Something happened, this was the original exception: %s" % to_native(e))

Inventory Plugins

They parse inventory sources. They also form a representation of the inventory in an in-memory style. They were added in the Ansible version 2.4.

Test plugins

They are used to verify the data. It is a feature of Jinja2 and is also available for use in Jinja2 templates, which are used by the template module. All the plugins can be easily extended, but the user can have several per file rather than having a file for each one. Almost all of the filter plugins shipped with the Ansible reside in core.py. They are very useful in conjunction with filter plugins such as map and select. They are also available for conditional directives such as when.

They don't use the standard configuration and documentation system.

Ansible evaluates the variables only when they are required, therefore the test plugins should propagate the exceptions jinja2.exceptions.UndefinedError as well as AnsibleUndefinedVariable to make sure that the undefined variables are only fatal when necessary.

try:
    cause_an_exception(with_undefined_variable)
except jinja2.exceptions.UndefinedError as e:
    raise AnsibleUndefinedVariable("Something happened, this was the original exception: %s" % to_native(e))
except Exception as e:
    raise AnsibleFilterError("Something happened, this was the original exception: %s" % to_native(e))

Vars plugins

They inject additional variable data into the ansible runs which didn't come from an inventory source, playbook, or command line. 'Host_vars" and "group_vars" are some of the playbook constructs that work using vars plugins. 

Though they were partially implemented in Ansible 2.0, they were rewritten so that they could be fully implemented in Ansible 2.4. They are supported from ansible 2.10 & newer.

Since ansible 2.4, they are executed as and when needed when preparing to execute a task. This helps reduce the extra cost of the 'always execute' behavior in the older versions. Since ansible 2.10, their execution can be toggled by the user to run either while preparing to execute or after importing an inventory source.

A user can create vars plugins using the REQUIRES_ENABLED class variable that is by default not enabled. In case the vars plugin resides in a collection, then it can't be enabled by default. The user must use REQUIRES_ENABLED in all the collections-based vars plugins. 

To make users enable the plugin, set the class variable REQUIRES_ENABLED:

class VarsModule(BaseVarsPlugin):
    REQUIRES_ENABLED = True

Also see, How to Check Python Version in CMD

Frequently Asked Questions

Which language should be used for writing plugins?

The plugins should be written in Python.

Why should the user convert the strings returned by the plugin into Python's Unicode type?

It is necessary to convert any strings returned by the plugin into Python's Unicode type so that these strings can run through Jinja2.

Mention any commonly used connection plugin.

One of the commonly used connection plugins is paramiko SSH.

Conclusion

In this article, we have extensively discussed Ansible-Developing Plugins.

After reading about the Ansible-Developing Plugins, are you not feeling excited to read/explore more articles on Development? Don't worry; Coding Ninjas has you covered. To learn about system design strategieshow to start learning full-stack web development and what are top web development languages.

If you want to enhance your skills in Data Structures and AlgorithmsCompetitive ProgrammingJavaScript, etc., you should check out our Guided path column at Coding Ninjas Studio. We at Coding Ninjas Studio organise many contests in which you can participate. You can also prepare for the contests and test your coding skills by taking the mock test series available. In case you have just started the learning process, and your dream is to crack major tech giants like Amazon, Microsoft, etc., then you should check out the most frequently asked problems and the interview experiences of your seniors that will surely help you in landing a job in your dream company. 

Do upvote if you find the blogs helpful.

Happy Learning!

Thank you image

Live masterclass