.. _flow_modules: ======= Modules ======= This in-depth dive helps you understand Ansible's program flow to execute modules. It is written for people working on the portions of the Core Ansible Engine that execute a module. Those writing Ansible Modules may also find this in-depth dive to be of interest, but individuals simply using Ansible Modules will not likely find this to be helpful. .. _flow_types_of_modules: Types of Modules ================ Ansible supports several different types of modules in its code base. Some of these are for backwards compatibility and others are to enable flexibility. .. _flow_action_plugins: Action Plugins -------------- Action Plugins look like modules to end users who are writing :term:`playbooks` but they're distinct entities for the purposes of this paper. Action Plugins always execute on the controller and are sometimes able to do all work there (for instance, the debug Action Plugin which prints some text for the user to see or the assert Action Plugin which can test whether several values in a playbook satisfy certain criteria.) More often, Action Plugins set up some values on the controller, then invoke an actual module on the managed node that does something with these values. An easy to understand version of this is the :ref:`template Action Plugin <template>`. The :ref:`template Action Plugin <template>` takes values from the user to construct a file in a temporary location on the controller using variables from the playbook environment. It then transfers the temporary file to a temporary file on the remote system. After that, it invokes the :ref:`copy module <copy>` which operates on the remote system to move the file into its final location, sets file permissions, and so on. .. _flow_new_style_modules: New-style Modules ----------------- All of the modules that ship with Ansible fall into this category. New-style modules have the arguments to the module embedded inside of them in some manner. Non-new-style modules must copy a separate file over to the managed node, which is less efficient as it requires two over-the-wire connections instead of only one. .. _flow_python_modules: Python ^^^^^^ New-style Python modules use the :ref:`ziploader` framework for constructing modules. All official modules (shipped with Ansible) use either this or the :ref:`powershell module framework <flow_powershell_modules>`. These modules use imports from :code:`ansible.module_utils` in order to pull in boilerplate module code, such as argument parsing, formatting of return values as :term:`JSON`, and various file operations. .. note:: In Ansible, up to version 2.0.x, the official Python modules used the :ref:`module_replacer` framework. For module authors, :ref:`ziploader` is largely a superset of :ref:`module_replacer` functionality, so you usually do not need to know about one versus the other. .. _flow_powershell_modules: Powershell ^^^^^^^^^^ New-style powershell modules use the :ref:`module_replacer` framework for constructing modules. These modules get a library of powershell code embedded in them before being sent to the managed node. .. _flow_jsonargs_modules: JSONARGS ^^^^^^^^ Scripts can arrange for an argument string to be placed within them by placing the string ``<<INCLUDE_ANSIBLE_MODULE_JSON_ARGS>>`` somewhere inside of the file. The module typically sets a variable to that value like this:: json_arguments = """<<INCLUDE_ANSIBLE_MODULE_JSON_ARGS>>""" Which is expanded as:: json_arguments = """{"param1": "test's quotes", "param2": "\"To be or not to be\" - Hamlet"}""" .. note:: Ansible outputs a :term:`JSON` string with bare quotes. Double quotes are used to quote string values, double quotes inside of string values are backslash escaped, and single quotes may appear unescaped inside of a string value. To use JSONARGS, your scripting language must have a way to handle this type of string. The example uses Python's triple quoted strings to do this. Other scripting languages may have a similar quote character that won't be confused by any quotes in the JSON or it may allow you to define your own start-of-quote and end-of-quote characters. If the language doesn't give you any of these then you'll need to write a :ref:`non-native JSON module <flow_want_json_modules>` or :ref:`Old-style module <flow_old_style_modules>` instead. The module typically parses the contents of ``json_arguments`` using a JSON library and then use them as native variables throughout the rest of its code. .. _flow_want_json_modules: Non-native want JSON modules ---------------------------- If a module has the string ``WANT_JSON`` in it anywhere, Ansible treats it as a non-native module that accepts a filename as its only command line parameter. The filename is for a temporary file containing a :term:`JSON` string containing the module's parameters. The module needs to open the file, read and parse the parameters, operate on the data, and print its return data as a JSON encoded dictionary to stdout before exiting. These types of modules are self-contained entities. As of Ansible 2.1, Ansible only modifies them to change a shebang line if present. .. seealso:: Examples of Non-native modules written in ruby are in the `Ansible for Rubyists <https://github.com/ansible/ansible-for-rubyists>`_ repository. .. _flow_old_style_modules: Old-style Modules ----------------- Old-style modules are similar to :ref:`want JSON modules <flow_want_json_modules>`, except that the file that they take contains ``key=value`` pairs for their parameters instead of :term:`JSON`. Ansible decides that a module is old-style when it doesn't have any of the markers that would show that it is one of the other types. .. _flow_how_modules_are_executed: How modules are executed ======================== When a user uses :program:`ansible` or :program:`ansible-playbook`, they specify a task to execute. The task is usually the name of a module along with several parameters to be passed to the module. Ansible takes these values and processes them in various ways before they are finally executed on the remote machine. .. _flow_executor_task_executor: executor/task_executor ---------------------- The TaskExecutor receives the module name and parameters that were parsed from the :term:`playbook <playbooks>` (or from the command line in the case of :command:`/usr/bin/ansible`). It uses the name to decide whether it's looking at a module or an :ref:`Action Plugin <flow_action_plugins>`. If it's a module, it loads the :ref:`Normal Action Plugin <flow_normal_action_plugin>` and passes the name, variables, and other information about the task and play to that Action Plugin for further processing. .. _flow_normal_action_plugin: Normal Action Plugin -------------------- The ``normal`` Action Plugin executes the module on the remote host. It is the primary coordinator of much of the work to actually execute the module on the managed machine. * It takes care of creating a connection to the managed machine by instantiating a Connection class according to the inventory configuration for that host. * It adds any internal Ansible variables to the module's parameters (for instance, the ones that pass along ``no_log`` to the module). * It takes care of creating any temporary files on the remote machine and cleans up afterwards. * It does the actual work of pushing the module and module parameters to the remote host, although the :ref:`module_common <flow_executor_module_common>` code described next does the work of deciding which format those will take. * It handles any special cases regarding modules (for instance, various complications around Windows modules that must have the same names as Python modules, so that internal calling of modules from other Action Plugins work.) Much of this functionality comes from the :class:`BaseAction` class, which lives in :file:`plugins/action/__init__.py`. It makes use of Connection and Shell objects to do its work. .. note:: When :term:`tasks <tasks>` are run with the ``async:`` parameter, Ansible uses the ``async`` Action Plugin instead of the ``normal`` Action Plugin to invoke it. That program flow is currently not documented. Read the source for information on how that works. .. _flow_executor_module_common: executor/module_common.py ------------------------- Code in :file:`executor/module_common.py` takes care of assembling the module to be shipped to the managed node. The module is first read in, then examined to determine its type. :ref:`PowerShell <flow_powershell_modules>` and :ref:`JSON-args modules <flow_jsonargs_modules>` are passed through :ref:`Module Replacer <module_replacer>`. New-style :ref:`Python modules <flow_python_modules>` are assembled by :ref:`ziploader`. :ref:`Non-native-want-JSON <flow_want_json_modules>` and :ref:`Old-Style modules <flow_old_style_modules>` aren't touched by either of these and pass through unchanged. After the assembling step, one final modification is made to all modules that have a shebang line. Ansible checks whether the interpreter in the shebang line has a specific path configured via an ``ansible_$X_interpreter`` inventory variable. If it does, Ansible substitutes that path for the interpreter path given in the module. After this Ansible returns the complete module data and the module type to the :ref:`Normal Action <_flow_normal_action_plugin>` which continues execution of the module. Next we'll go into some details of the two assembler frameworks. .. _module_replacer: Module Replacer ^^^^^^^^^^^^^^^ The Module Replacer is essentially a preprocessor (like the C Preprocessor for those familiar with that language). It does straight substitutions of specific substring patterns in the module file. There are two types of substitutions: * Replacements that only happen in the module file. These are public replacement strings that modules can utilize to get helpful boilerplate or access to arguments. - :code:`from ansible.module_utils.MOD_LIB_NAME import *` is replaced with the contents of the :file:`ansible/module_utils/MOD_LIB_NAME.py` These should only be used with :ref:`new-style Python modules <flow_python_modules>`. - :code:`#<<INCLUDE_ANSIBLE_MODULE_COMMON>>` is equivalent to :code:`from ansible.module_utils.basic import *` and should also only apply to new-style Python modules. - :code:`# POWERSHELL_COMMON` substitutes the contents of :file:`ansible/module_utils/powershell.ps1`. It should only be used with :ref:`new-style Powershell modules <flow_powershell_modules>`. * Replacements that are used by ``ansible.module_utils`` code. These are internal replacement patterns. They may be used internally, in the above public replacements, but shouldn't be used directly by modules. - :code:`"<<ANSIBLE_VERSION>>"` is substituted with the Ansible version. In a new-style Python module, it's better to use ``from ansible import __version__`` and then use ``__version__`` instead. - :code:`"<<INCLUDE_ANSIBLE_MODULE_COMPLEX_ARGS>>"` is substituted with a string which is the Python ``repr`` of the :term:`JSON` encoded module parameters. Using ``repr`` on the JSON string makes it safe to embed in a Python file. In :ref:`new-style Python modules <flow_python_modules>` under :ref:`ziploader` this is passed in via an environment variable instead. - :code:`<<SELINUX_SPECIAL_FILESYSTEMS>>` substitutes a string which is a comma separated list of file systems which have a file system dependent security context in SELinux. In new-style Python modules, this is found by looking up ``SELINUX_SPECIAL_FS`` from the :envvar:`ANSIBLE_MODULE_CONSTANTS` environment variable. See the :ref:`ziploader` documentation for details. - :code:`<<INCLUDE_ANSIBLE_MODULE_JSON_ARGS>>` substitutes the module parameters as a JSON string. Care must be taken to properly quote the string as JSON data may contain quotes. This pattern is not substituted in new-style Python modules as they can get the module parameters via the environment variable. - the string :code:`syslog.LOG_USER` is replaced wherever it occurs with the value of ``syslog_facility`` from the :file:`ansible.cfg` or any ``ansible_syslog_facility`` inventory variable that applies to this host. In new-style Python modules, you can get the value of the ``syslog_facility`` by looking up ``SYSLOG_FACILITY`` in the :envvar:`ANSIBLE_MODULE_CONSTANTS` environment variable. See the :ref:`ziploader` documentation for details. .. _ziploader: ziploader ^^^^^^^^^ Ziploader differs from :ref:`module_replacer` in that it uses real Python imports of things in module_utils instead of merely preprocessing the module. It does this by constructing a zipfile--which includes the module file, files in :file:`ansible/module_utils` that are imported by the module, and some boilerplate to pass in the constants. The zipfile is then Base64 encoded and wrapped in a small Python script which decodes the Base64 encoding and places the zipfile into a temp direcrtory on the managed node. It then extracts just the ansible module script from the zip file and places that in the temporary directory as well. Then it sets the PYTHONPATH to find python modules inside of the zip file and invokes :command:`python` on the extracted ansible module. .. note:: Ansible wraps the zipfile in the Python script for two reasons: * for compatibility with Python-2.4 and Python-2.6 which have less featureful versions of Python's ``-m`` command line switch. * so that pipelining will function properly. Pipelining needs to pipe the Python module into the Python interpreter on the remote node. Python understands scripts on stdin but does not understand zip files. In ziploader, any imports of Python modules from the ``ansible.module_utils`` package trigger inclusion of that Python file into the zipfile. Instances of :code:`#<<INCLUDE_ANSIBLE_MODULE_COMMON>>` in the module are turned into :code:`from ansible.module_utils.basic import *` and :file:`ansible/module-utils/basic.py` is then included in the zipfile. Files that are included from module_utils are themselves scanned for imports of other Python modules from module_utils to be included in the zipfile as well. .. warning:: At present, Ziploader cannot determine whether an import should be included if it is a relative import. Always use an absolute import that has ``ansible.module_utils`` in it to allow ziploader to determine that the file should be included. .. _flow_passing_module_args: Passing args ~~~~~~~~~~~~ In :ref:`module_replacer`, module arguments are turned into a JSON-ified string and substituted into the combined module file. In :ref:`ziploader`, the JSON-ified string is passed into the module via stdin. When a :class:`ansible.module_utils.basic.AnsibleModule` is instantiated, it parses this string and places the args into :attr:`AnsibleModule.params` where it can be accessed by the module's other code. .. _flow_passing_module_constants: Passing constants ~~~~~~~~~~~~~~~~~ Currently, there are three constants passed from the controller to the modules: ``ANSIBLE_VERSION``, ``SELINUX_SPECIAL_FS``, and ``SYSLOG_FACILITY``. In :ref:`module_replacer`, ``ANSIBLE_VERSION`` and ``SELINUX_SPECIAL_FS`` were substituted into the global variables :code:`ansible.module_utils.basic.ANSIBLE_VERSION` and :code:`ansible.module_utils.basic.SELINUX_SPECIAL_FS`. ``SYSLOG_FACILITY`` didn't get placed into a variable. Instead, any occurrences of the string ``syslog.LOG_USER`` in the combined module file were replaced with ``syslog.`` followed by the string contained in ``SYSLOG_FACILITY``. All of these have changed in :ref:`ziploader`. The Ansible verison can now be used by a module by importing ``__version__`` from ansible:: from ansible import __version__ module.exit_json({'msg': 'module invoked by ansible %s' % __version__}) For now, :code:`ANSIBLE_VERSION` is also available at its old location inside of ``ansible.module_utils.basic``, but that will eventually be removed. ``SELINUX_SPECIAL_FS`` and ``SYSLOG_FACILITY`` have changed much more. :ref:`ziploader` passes these as part of the JSON-ified argument string via stdin. When :class:`ansible.module_utils.basic.AnsibleModule` is instantiated, it parses this string and places the constants into :attr:`AnsibleModule.constants` where other code can access it. Unlike the ``ANSIBLE_VERSION``, where some efforts were made to keep the old backwards compatible globals available, these two constants are not available at their old names. This is a combination of the degree to which these are internal to the needs of ``module_utils.basic`` and, in the case of ``SYSLOG_FACILITY``, how hacky and unsafe the previous implementation was. Porting code from the :ref:`module_replacer` method of getting ``SYSLOG_FACILITY`` to the new one is a little more tricky than the other constants and args, due to just how hacky the old way was. Here's an example of using it in the new way:: import syslog facility_name = module.constants.get('SYSLOG_FACILITY') facility = getattr(syslog, facility_name) syslog.openlog(str(module), 0, facility) .. _flow_special_considerations: Special Considerations ---------------------- .. _flow_pipelining: Pipelining ^^^^^^^^^^ Ansible can transfer a module to a remote machine in one of two ways: * it can write out the module to a temporary file on the remote host and then use a second connection to the remote host to execute it with the interpreter that the module needs * or it can use what's known as pipelining to execute the module by piping it into the remote interpreter's stdin. Pipelining only works with modules written in Python at this time because Ansible only knows that Python supports this mode of operation. Supporting pipelining means that whatever format the module payload takes before being sent over the wire must be executable by Python via stdin.