Update the Ansiballz developer docs

pull/13384/merge
Toshio Kuratomi 9 years ago
parent 0edec45c3d
commit 7fb0f75db6

@ -24,10 +24,10 @@ Action Plugins
-------------- --------------
Action Plugins look like modules to end users who are writing :term:`playbooks` but 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 they're distinct entities for the purposes of this document. Action Plugins
always execute on the controller and are sometimes able to do all work there 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 (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 see or the ``assert`` Action Plugin which can test whether several values in
a playbook satisfy certain criteria.) a playbook satisfy certain criteria.)
More often, Action Plugins set up some values on the controller, then invoke an More often, Action Plugins set up some values on the controller, then invoke an
@ -57,7 +57,7 @@ connections instead of only one.
Python Python
^^^^^^ ^^^^^^
New-style Python modules use the :ref:`ziploader` framework for constructing New-style Python modules use the :ref:`Ansiballz` framework for constructing
modules. All official modules (shipped with Ansible) use either this or the modules. All official modules (shipped with Ansible) use either this or the
:ref:`powershell module framework <flow_powershell_modules>`. :ref:`powershell module framework <flow_powershell_modules>`.
@ -66,7 +66,7 @@ boilerplate module code, such as argument parsing, formatting of return
values as :term:`JSON`, and various file operations. values as :term:`JSON`, and various file operations.
.. note:: In Ansible, up to version 2.0.x, the official Python modules used the .. 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 :ref:`module_replacer` framework. For module authors, :ref:`Ansiballz` is
largely a superset of :ref:`module_replacer` functionality, so you usually largely a superset of :ref:`module_replacer` functionality, so you usually
do not need to know about one versus the other. do not need to know about one versus the other.
@ -127,6 +127,25 @@ only modifies them to change a shebang line if present.
.. seealso:: Examples of Non-native modules written in ruby are in the `Ansible .. seealso:: Examples of Non-native modules written in ruby are in the `Ansible
for Rubyists <https://github.com/ansible/ansible-for-rubyists>`_ repository. for Rubyists <https://github.com/ansible/ansible-for-rubyists>`_ repository.
.. _flow_binary_modules:
Binary Modules
--------------
From Ansible 2.2 onwards, modules may also be small binary programs. Ansible
doesn't perform any magic to make these portable to different systems so they
may be specific to the system on which they were compiled or require other
binary runtime dependencies. Despite these drawbacks, a site may sometimes
have no choice but to compile a custom module against a specific binary
library if that's the only way they have to get access to certain resources.
Binary modules take their arguments and will return data to Ansible in the same
way as :ref:`want JSON modules <flow_want_json_modules>`.
.. seealso:: One example of a `binary module
<https://github.com/ansible/ansible/blob/devel/test/integration/library/helloworld.go>`_
written in go.
.. _flow_old_style_modules: .. _flow_old_style_modules:
Old-style Modules Old-style Modules
@ -174,22 +193,23 @@ the primary coordinator of much of the work to actually execute the module on
the managed machine. the managed machine.
* It takes care of creating a connection to the managed machine by * It takes care of creating a connection to the managed machine by
instantiating a Connection class according to the inventory configuration for instantiating a ``Connection`` class according to the inventory
that host. configuration for that host.
* It adds any internal Ansible variables to the module's parameters (for * It adds any internal Ansible variables to the module's parameters (for
instance, the ones that pass along ``no_log`` to the module). instance, the ones that pass along ``no_log`` to the module).
* It takes care of creating any temporary files on the remote machine and * It takes care of creating any temporary files on the remote machine and
cleans up afterwards. cleans up afterwards.
* It does the actual work of pushing the module and module parameters to the * 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>` remote host, although the :ref:`module_common <flow_executor_module_common>`
code described next does the work of deciding which format those will take. code described in the next section does the work of deciding which format
those will take.
* It handles any special cases regarding modules (for instance, various * It handles any special cases regarding modules (for instance, various
complications around Windows modules that must have the same names as Python complications around Windows modules that must have the same names as Python
modules, so that internal calling of modules from other Action Plugins work.) modules, so that internal calling of modules from other Action Plugins work.)
Much of this functionality comes from the :class:`BaseAction` class, Much of this functionality comes from the :class:`BaseAction` class,
which lives in :file:`plugins/action/__init__.py`. It makes use of Connection which lives in :file:`plugins/action/__init__.py`. It makes use of
and Shell objects to do its work. ``Connection`` and ``Shell`` objects to do its work.
.. note:: .. note::
When :term:`tasks <tasks>` are run with the ``async:`` parameter, Ansible When :term:`tasks <tasks>` are run with the ``async:`` parameter, Ansible
@ -207,15 +227,16 @@ 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 to determine its type. :ref:`PowerShell <flow_powershell_modules>` and
:ref:`JSON-args modules <flow_jsonargs_modules>` are passed through :ref:`JSON-args modules <flow_jsonargs_modules>` are passed through
:ref:`Module Replacer <module_replacer>`. New-style :ref:`Module Replacer <module_replacer>`. New-style
:ref:`Python modules <flow_python_modules>` are assembled by :ref:`ziploader`. :ref:`Python modules <flow_python_modules>` are assembled by :ref:`Ansiballz`.
:ref:`Non-native-want-JSON <flow_want_json_modules>` and :ref:`Non-native-want-JSON <flow_want_json_modules>`,
:ref:`Binary modules <flow_binary_modules>`, and
:ref:`Old-Style modules <flow_old_style_modules>` aren't touched by either of :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 these and pass through unchanged. After the assembling step, one final
modification is made to all modules that have a shebang line. Ansible checks 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 whether the interpreter in the shebang line has a specific path configured via
an ``ansible_$X_interpreter`` inventory variable. If it does, Ansible an ``ansible_$X_interpreter`` inventory variable. If it does, Ansible
substitutes that path for the interpreter path given in the module. After 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 this, Ansible returns the complete module data and the module type to the
:ref:`Normal Action <_flow_normal_action_plugin>` which continues execution of :ref:`Normal Action <_flow_normal_action_plugin>` which continues execution of
the module. the module.
@ -226,8 +247,9 @@ 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 The Module Replacer framework is the original framework implementing new-style
those familiar with that language). It does straight substitutions of modules. It is essentially a preprocessor (like the C Preprocessor for those
familiar with that programming language). It does straight substitutions of
specific substring patterns in the module file. There are two types of specific substring patterns in the module file. There are two types of
substitutions: substitutions:
@ -250,47 +272,56 @@ substitutions:
replacements, but shouldn't be used directly by modules. replacements, but shouldn't be used directly by modules.
- :code:`"<<ANSIBLE_VERSION>>"` is substituted with the Ansible version. In - :code:`"<<ANSIBLE_VERSION>>"` is substituted with the Ansible version. In
a new-style Python module, it's better to use ``from ansible import :ref:`new-style Python modules <flow_python_modules>` under the
__version__`` and then use ``__version__`` instead. :ref:`Ansiballz` frameworkthe proper way is to instead instantiate an
:class:`AnsibleModule` and then access the version from
:attr:``AnsibleModule.ansible_version``.
- :code:`"<<INCLUDE_ANSIBLE_MODULE_COMPLEX_ARGS>>"` is substituted with - :code:`"<<INCLUDE_ANSIBLE_MODULE_COMPLEX_ARGS>>"` is substituted with
a string which is the Python ``repr`` of the :term:`JSON` encoded module 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 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>` a Python file. In new-style Python modules under the Ansiballz framework
under :ref:`ziploader` this is passed in via an environment variable this is better accessed by instantiating an :class:`AnsibleModule` and
instead. then using :attr:`AnsibleModule.params`.
- :code:`<<SELINUX_SPECIAL_FILESYSTEMS>>` substitutes a string which is - :code:`<<SELINUX_SPECIAL_FILESYSTEMS>>` substitutes a string which is
a comma separated list of file systems which have a file system dependent 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 security context in SELinux. In new-style Python modules, if you really
by looking up ``SELINUX_SPECIAL_FS`` from the need this you should instantiate an :class:`AnsibleModule` and then use
:envvar:`ANSIBLE_MODULE_CONSTANTS` environment variable. See the :attr:`AnsibleModule._selinux_special_fs`. The variable has also changed
:ref:`ziploader` documentation for details. from a comma separated string of file system names to an actual python
list of filesystem names.
- :code:`<<INCLUDE_ANSIBLE_MODULE_JSON_ARGS>>` substitutes the module - :code:`<<INCLUDE_ANSIBLE_MODULE_JSON_ARGS>>` substitutes the module
parameters as a JSON string. Care must be taken to properly quote the 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 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 in new-style Python modules as they can get the module parameters another
environment variable. way.
- the string :code:`syslog.LOG_USER` is replaced wherever it occurs with the - The string :code:`syslog.LOG_USER` is replaced wherever it occurs with the
value of ``syslog_facility`` from the :file:`ansible.cfg` or any ``syslog_facility`` which was named in :file:`ansible.cfg` or any
``ansible_syslog_facility`` inventory variable that applies to this host. In ``ansible_syslog_facility`` inventory variable that applies to this host. In
new-style Python modules, you can get the value of the ``syslog_facility`` new-style Python modules this has changed slightly. If you really need to
by looking up ``SYSLOG_FACILITY`` in the :envvar:`ANSIBLE_MODULE_CONSTANTS` access it, you should instantiate an :class:`AnsibleModule` and then use
environment variable. See the :ref:`ziploader` documentation for details. :attr:`AnsibleModule._syslog_facility` to access it. It is no longer the
actual syslog facility and is now the name of the syslog facility. See
the :ref:`documentation on internal arguments <flow_internal_arguments>`
for details.
.. _ziploader: .. _Ansiballz:
ziploader Ansiballz
^^^^^^^^^ ^^^^^^^^^
Ziploader differs from :ref:`module_replacer` in that it uses real Python Ansible 2.1 switched from the :ref:`module_replacer` framework to the
imports of things in module_utils instead of merely preprocessing the module. Ansiballz framework for assembling modules. The Ansiballz framework differs
It does this by constructing a zipfile--which includes the module file, files from module replacer in that it uses real Python imports of things in
:file:`ansible/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 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 boilerplate to pass in the module's parameters. The zipfile is then Base64
wrapped in a small Python script which decodes the Base64 encoding and places encoded and wrapped in a small Python script which decodes the Base64 encoding
the zipfile into a temp direcrtory on the managed node. It then extracts just and places the zipfile into a temp directory on the managed node. It then
the ansible module script from the zip file and places that in the temporary extracts just the ansible module script from the zip file and places that in
directory as well. Then it sets the PYTHONPATH to find python modules inside the temporary directory as well. Then it sets the PYTHONPATH to find python
of the zip file and invokes :command:`python` on the extracted ansible module. modules inside of the zip file and invokes :command:`python` on the extracted
ansible module.
.. note:: .. note::
Ansible wraps the zipfile in the Python script for two reasons: Ansible wraps the zipfile in the Python script for two reasons:
@ -301,19 +332,20 @@ of the zip file and invokes :command:`python` on the extracted ansible module.
Python module into the Python interpreter on the remote node. Python Python module into the Python interpreter on the remote node. Python
understands scripts on stdin but does not understand zip files. understands scripts on stdin but does not understand zip files.
In ziploader, any imports of Python modules from the ``ansible.module_utils`` In Ansiballz, any imports of Python modules from the
package trigger inclusion of that Python file into the zipfile. Instances of :py:mod:`ansible.module_utils` package trigger inclusion of that Python file
:code:`#<<INCLUDE_ANSIBLE_MODULE_COMMON>>` in the module are turned into into the zipfile. Instances of :code:`#<<INCLUDE_ANSIBLE_MODULE_COMMON>>` in
:code:`from ansible.module_utils.basic import *` and the module are turned into :code:`from ansible.module_utils.basic import *`
:file:`ansible/module-utils/basic.py` is then included in the zipfile. Files and :file:`ansible/module-utils/basic.py` is then included in the zipfile.
that are included from module_utils are themselves scanned for imports of other Files that are included from :file:`module_utils` are themselves scanned for
Python modules from module_utils to be included in the zipfile as well. imports of other Python modules from :file:`module_utils` to be included in
the zipfile as well.
.. warning:: .. warning::
At present, Ziploader cannot determine whether an import should be At present, the Ansiballz Framework cannot determine whether an import
included if it is a relative import. Always use an absolute import that should be included if it is a relative import. Always use an absolute
has ``ansible.module_utils`` in it to allow ziploader to determine that import that has :py:mod:`ansible.module_utils` in it to allow Ansiballz to
the file should be included. determine that the file should be included.
.. _flow_passing_module_args: .. _flow_passing_module_args:
@ -321,60 +353,133 @@ Passing args
~~~~~~~~~~~~ ~~~~~~~~~~~~
In :ref:`module_replacer`, module arguments are turned into a JSON-ified In :ref:`module_replacer`, module arguments are turned into a JSON-ified
string and substituted into the combined module file. In :ref:`ziploader`, string and substituted into the combined module file. In :ref:`Ansiballz`,
the JSON-ified string is passed into the module via stdin. When the JSON-ified string is passed into the module via stdin. When
a :class:`ansible.module_utils.basic.AnsibleModule` is instantiated, a :class:`ansible.module_utils.basic.AnsibleModule` is instantiated,
it parses this string and places the args into it parses this string and places the args into
:attr:`AnsibleModule.params` where it can be accessed by the module's :attr:`AnsibleModule.params` where it can be accessed by the module's
other code. other code.
.. _flow_passing_module_constants: .. note::
Internally, the :class:`AnsibleModule` uses the helper function,
Passing constants :py:func:`ansible.module_utils.basic._load_params`, to load the parameters
~~~~~~~~~~~~~~~~~ from stdin and save them into an internal global variable. Very dynamic
custom modules which need to parse the parameters prior to instantiating
Currently, there are three constants passed from the controller to the modules: an ``AnsibleModule`` may use ``_load_params`` to retrieve the
``ANSIBLE_VERSION``, ``SELINUX_SPECIAL_FS``, and ``SYSLOG_FACILITY``. In parameters. Be aware that ``_load_params`` is an internal function and
:ref:`module_replacer`, ``ANSIBLE_VERSION`` and ``SELINUX_SPECIAL_FS`` were may change in breaking ways if necessary to support changes in the code.
substituted into the global variables However, we'll do our best not to break it gratuitously, which is not
:code:`ansible.module_utils.basic.ANSIBLE_VERSION` and something that can be said for either the way parameters are passed or
:code:`ansible.module_utils.basic.SELINUX_SPECIAL_FS`. ``SYSLOG_FACILITY`` didn't the internal global variable.
get placed into a variable. Instead, any occurrences of the string
``syslog.LOG_USER`` in the combined module file were replaced with ``syslog.`` .. _flow_internal_arguments:
followed by the string contained in ``SYSLOG_FACILITY``. All of these have
changed in :ref:`ziploader`. Internal arguments
^^^^^^^^^^^^^^^^^^
The Ansible verison can now be used by a module by importing ``__version__``
from ansible:: Both :ref:`module replacer` and :ref:`Ansiballz` send additional arguments to
the module beyond those which the user specified in the playbook. These
from ansible import __version__ additional arguments are internal parameters that help implement global
module.exit_json({'msg': 'module invoked by ansible %s' % __version__}) Ansible features. Modules often do not need to know about these explicitly as
the features are implemented in :py:mod:`ansible.module_utils.basic` but certain
For now, :code:`ANSIBLE_VERSION` is also available at its old location inside of features need support from the module so it's good to know about them.
``ansible.module_utils.basic``, but that will eventually be removed.
_ansible_no_log
``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 This is a boolean. If it's True then the playbook specified ``no_log`` (in
:class:`ansible.module_utils.basic.AnsibleModule` is instantiated, it parses this a task's parameters or as a play parameter). This automatically affects calls
string and places the constants into :attr:`AnsibleModule.constants` to :py:meth:`AnsibleModule.log`. If a module implements its own logging then
where other code can access it. it needs to check this value. The best way to look at this is for the module
to instantiate an :class:`AnsibleModule` and then check the value of
Unlike the ``ANSIBLE_VERSION``, where some efforts were made to keep the old :attr:`AnsibleModule.no_log`.
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 .. note::
internal to the needs of ``module_utils.basic`` and, in the case of ``no_log`` specified in a module's argument_spec are handled by a different mechanism.
``SYSLOG_FACILITY``, how hacky and unsafe the previous implementation was.
_ansible_debug
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 This is a boolean that turns on more verbose logging. If a module uses
of using it in the new way:: :py:meth:`AnsibleModule.debug` rather than :py:meth:`AnsibleModule.log` then
the messages are only logged if this is True. This also turns on logging of
external commands that the module executes. This can be changed via
the``debug`` setting in :file:`ansible.cfg` or the environment variable
:envvar:`ANSIBLE_DEBUG`. If, for some reason, a module must access this, it
should do so by instantiating an :class:`AnsibleModule` and accessing
:attr:`AnsibleModule._debug`.
_ansible_diff
~~~~~~~~~~~~~
This boolean is turned on via the ``--diff`` command line option. If a module
supports it, it will tell the module to show a unified diff of changes to be
made to templated files. The proper way for a module to access this is by
instantiating an :class:`AnsibleModule` and accessing
:attr:`AnsibleModule._diff`.
_ansible_verbosity
~~~~~~~~~~~~~~~~~~
This value could be used for finer grained control over logging. However, it
is currently unused.
_ansible_selinux_special_fs
~~~~~~~~~~~~~~~~~~~~~~~~~~~
This is a list of names of filesystems which should have a special selinux
context. They are used by the :class:`AnsibleModule` methods which operate on
files (changing attributes, moving, and copying). The list of names is set
via a comma separated string of filesystem names from :file:`ansible.cfg`::
# ansible.cfg
[selinux]
special_context_filesystems=nfs,vboxsf,fuse,ramfs
If a module cannot use the builtin ``AnsibleModule`` methods to manipulate
files and needs to know about these special context filesystems, it should
instantiate an ``AnsibleModule`` and then examine the list in
:attr:`AnsibleModule._selinux_special_fs`.
This replaces :attr:`ansible.module_utils.basic.SELINUX_SPECIAL_FS` from
:ref:`module_replacer`. In module replacer it was a comma separated string of
filesystem names. Under Ansiballz it's an actual list.
.. versionadded:: 2.1
_ansible_syslog_facility
~~~~~~~~~~~~~~~~~~~~~~~~
This parameter controls which syslog facility ansible module logs to. It may
be set by changing the ``syslog_facility`` value in :file:`ansible.cfg`. Most
modules should just use :meth:`AnsibleModule.log` which will then make use of
this. If a module has to use this on its own, it should instantiate an
:class:`AnsibleModule` and then retrieve the name of the syslog facility from
:attr:`AnsibleModule._syslog_facility`. The code will look slightly different
than it did under :ref:`module_replacer` due to how hacky the old way was::
# Old way
import syslog
syslog.openlog(NAME, 0, syslog.LOG_USER)
# New way
import syslog import syslog
facility_name = module.constants.get('SYSLOG_FACILITY') facility_name = module._syslog_facility
facility = getattr(syslog, facility_name) facility = getattr(syslog, facility_name, syslog.LOG_USER)
syslog.openlog(str(module), 0, facility) syslog.openlog(NAME, 0, facility)
.. versionadded:: 2.1
_ansible_version
~~~~~~~~~~~~~~~~
This parameter passes the version of ansible that runs the module. To access
it, a module should instantiate an :class:`AnsibleModule` and then retrieve it
from :attr:`AnsibleModule.ansible_version`. This replaces
:attr:`ansible.module_utils.basic.ANSIBLE_VERSION` from
:ref:`module_replacer`.
.. versionadded:: 2.1
.. _flow_special_considerations: .. _flow_special_considerations:
@ -398,3 +503,21 @@ Pipelining only works with modules written in Python at this time because
Ansible only knows that Python supports this mode of operation. Supporting Ansible only knows that Python supports this mode of operation. Supporting
pipelining means that whatever format the module payload takes before being pipelining means that whatever format the module payload takes before being
sent over the wire must be executable by Python via stdin. sent over the wire must be executable by Python via stdin.
.. _flow_args_over_stdin:
Why pass args over stdin?
^^^^^^^^^^^^^^^^^^^^^^^^^
Passing arguments via stdin was chosen for the following reasons:
* When combined with :ref:`pipelining`, this keeps the module's arguments from
temporarily being saved onto disk on the remote machine. This makes it
harder (but not impossible) for a malicious user on the remote machine to
steal any sensitive information that may be present in the arguments.
* Command line arguments would be insecure as most systems allow unprivileged
users to read the full commandline of a process.
* Environment variables are usually more secure than the commandline but some
systems limit the total size of the environment. This could lead to
truncation of the parameters if we hit that limit.

Loading…
Cancel
Save