123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695 |
- .. _writing-execution-modules:
- =========================
- Writing Execution Modules
- =========================
- Salt execution modules are the functions called by the :command:`salt` command.
- Modules Are Easy to Write!
- ==========================
- Writing Salt execution modules is straightforward.
- A Salt execution module is a Python or `Cython`_ module placed in a directory
- called ``_modules/`` at the root of the Salt fileserver. When using the default
- fileserver backend (i.e. :py:mod:`roots <salt.fileserver.roots>`), unless
- environments are otherwise defined in the :conf_master:`file_roots` config
- option, the ``_modules/`` directory would be located in ``/srv/salt/_modules``
- on most systems.
- Modules placed in ``_modules/`` will be synced to the minions when any of the
- following Salt functions are called:
- * :mod:`state.highstate <salt.modules.state.highstate>` (or :mod:`state.apply
- <salt.modules.state.apply_>` with no state argument)
- * :mod:`saltutil.sync_modules <salt.modules.saltutil.sync_modules>`
- * :mod:`saltutil.sync_all <salt.modules.saltutil.sync_all>`
- Modules placed in ``_modules/`` will be synced to masters when any of the
- following Salt runners are called:
- * :mod:`saltutil.sync_modules <salt.runners.saltutil.sync_modules>`
- * :mod:`saltutil.sync_all <salt.runners.saltutil.sync_all>`
- Note that a module's default name is its filename
- (i.e. ``foo.py`` becomes module ``foo``), but that its name can be overridden
- by using a :ref:`__virtual__ function <virtual-modules>`.
- If a Salt module has errors and cannot be imported, the Salt minion will continue
- to load without issue and the module with errors will simply be omitted.
- If adding a Cython module the file must be named ``<modulename>.pyx`` so that
- the loader knows that the module needs to be imported as a Cython module. The
- compilation of the Cython module is automatic and happens when the minion
- starts, so only the ``*.pyx`` file is required.
- .. _`Cython`: https://cython.org/
- Zip Archives as Modules
- =======================
- Python 2.3 and higher allows developers to directly import zip archives
- containing Python code. By setting :conf_minion:`enable_zip_modules` to
- ``True`` in the minion config, the Salt loader will be able to import ``.zip``
- files in this fashion. This allows Salt module developers to package
- dependencies with their modules for ease of deployment, isolation, etc.
- For a user, Zip Archive modules behave just like other modules. When executing
- a function from a module provided as the file ``my_module.zip``, a user would
- call a function within that module as ``my_module.<function>``.
- Creating a Zip Archive Module
- -----------------------------
- A Zip Archive module is structured similarly to a simple `Python package`_.
- The ``.zip`` file contains a single directory with the same name as the module.
- The module code traditionally in ``<module_name>.py`` goes in
- ``<module_name>/__init__.py``. The dependency packages are subdirectories of
- ``<module_name>/``.
- Here is an example directory structure for the ``lumberjack`` module, which has
- two library dependencies (``sleep`` and ``work``) to be included.
- .. code-block:: bash
- modules $ ls -R lumberjack
- __init__.py sleep work
- lumberjack/sleep:
- __init__.py
- lumberjack/work:
- __init__.py
- The contents of ``lumberjack/__init__.py`` show how to import and use these
- included libraries.
- .. code-block:: python
- # Libraries included in lumberjack.zip
- from lumberjack import sleep, work
- def is_ok(person):
- """ Checks whether a person is really a lumberjack """
- return sleep.all_night(person) and work.all_day(person)
- Then, create the zip:
- .. code-block:: console
- modules $ zip -r lumberjack lumberjack
- adding: lumberjack/ (stored 0%)
- adding: lumberjack/__init__.py (deflated 39%)
- adding: lumberjack/sleep/ (stored 0%)
- adding: lumberjack/sleep/__init__.py (deflated 7%)
- adding: lumberjack/work/ (stored 0%)
- adding: lumberjack/work/__init__.py (deflated 7%)
- modules $ unzip -l lumberjack.zip
- Archive: lumberjack.zip
- Length Date Time Name
- -------- ---- ---- ----
- 0 08-21-15 20:08 lumberjack/
- 348 08-21-15 20:08 lumberjack/__init__.py
- 0 08-21-15 19:53 lumberjack/sleep/
- 83 08-21-15 19:53 lumberjack/sleep/__init__.py
- 0 08-21-15 19:53 lumberjack/work/
- 81 08-21-15 19:21 lumberjack/work/__init__.py
- -------- -------
- 512 6 files
- Once placed in :conf_master:`file_roots`, Salt users can distribute and use
- ``lumberjack.zip`` like any other module.
- .. code-block:: bash
- $ sudo salt minion1 saltutil.sync_modules
- minion1:
- - modules.lumberjack
- $ sudo salt minion1 lumberjack.is_ok 'Michael Palin'
- minion1:
- True
- .. _`Python package`: https://docs.python.org/2/tutorial/modules.html#packages
- .. _cross-calling-execution-modules:
- Cross Calling Execution Modules
- ===============================
- All of the Salt execution modules are available to each other and modules can
- call functions available in other execution modules.
- The variable ``__salt__`` is packed into the modules after they are loaded into
- the Salt minion.
- The ``__salt__`` variable is a :ref:`Python dictionary <python:typesmapping>`
- containing all of the Salt functions. Dictionary keys are strings representing
- the names of the modules and the values are the functions themselves.
- Salt modules can be cross-called by accessing the value in the ``__salt__``
- dict:
- .. code-block:: python
- def foo(bar):
- return __salt__["cmd.run"](bar)
- This code will call the `run` function in the :mod:`cmd <salt.modules.cmdmod>`
- module and pass the argument ``bar`` to it.
- Calling Execution Modules on the Salt Master
- ============================================
- .. versionadded:: 2016.11.0
- Execution modules can now also be called via the :command:`salt-run` command
- using the :ref:`salt runner <salt_salt_runner>`.
- Preloaded Execution Module Data
- ===============================
- When interacting with execution modules often it is nice to be able to read
- information dynamically about the minion or to load in configuration parameters
- for a module.
- Salt allows for different types of data to be loaded into the modules by the
- minion.
- Grains Data
- -----------
- The values detected by the Salt Grains on the minion are available in a
- :ref:`Python dictionary <python:typesmapping>` named ``__grains__`` and can be
- accessed from within callable objects in the Python modules.
- To see the contents of the grains dictionary for a given system in your
- deployment run the :func:`grains.items` function:
- .. code-block:: bash
- salt 'hostname' grains.items --output=pprint
- Any value in a grains dictionary can be accessed as any other Python
- dictionary. For example, the grain representing the minion ID is stored in the
- ``id`` key and from an execution module, the value would be stored in
- ``__grains__['id']``.
- Module Configuration
- --------------------
- Since parameters for configuring a module may be desired, Salt allows for
- configuration information from the minion configuration file to be passed to
- execution modules.
- Since the minion configuration file is a YAML document, arbitrary configuration
- data can be passed in the minion config that is read by the modules. It is
- therefore **strongly** recommended that the values passed in the configuration
- file match the module name. A value intended for the ``test`` execution module
- should be named ``test.<value>``.
- The test execution module contains usage of the module configuration and the
- default configuration file for the minion contains the information and format
- used to pass data to the modules. :mod:`salt.modules.test`,
- :file:`conf/minion`.
- .. _module_init:
- ``__init__`` Function
- ---------------------
- If you want your module to have different execution modes based on minion
- configuration, you can use the ``__init__(opts)`` function to perform initial
- module setup. The parameter ``opts`` is the complete minion configuration,
- as also available in the ``__opts__`` dict.
- .. code-block:: python
- """
- Cheese module initialization example
- """
- def __init__(opts):
- """
- Allow foreign imports if configured to do so
- """
- if opts.get("cheese.allow_foreign", False):
- _enable_foreign_products()
- Strings and Unicode
- ===================
- An execution module author should always assume that strings fed to the module
- have already decoded from strings into Unicode. In Python 2, these will
- be of type 'Unicode' and in Python 3 they will be of type ``str``. Calling
- from a state to other Salt sub-systems, should pass Unicode (or bytes if passing binary data). In the
- rare event that a state needs to write directly to disk, Unicode should be
- encoded to a string immediately before writing to disk. An author may use
- ``__salt_system_encoding__`` to learn what the encoding type of the system is.
- For example, `'my_string'.encode(__salt_system_encoding__')`.
- Outputter Configuration
- =======================
- Since execution module functions can return different data, and the way the
- data is printed can greatly change the presentation, Salt allows for a specific
- outputter to be set on a function-by-function basis.
- This is done be declaring an ``__outputter__`` dictionary in the global scope
- of the module. The ``__outputter__`` dictionary contains a mapping of function
- names to Salt :ref:`outputters <all-salt.output>`.
- .. code-block:: python
- __outputter__ = {"run": "txt"}
- This will ensure that the ``txt`` outputter is used to display output from the
- ``run`` function.
- .. _virtual-modules:
- Virtual Modules
- ===============
- Virtual modules let you override the name of a module in order to use the same
- name to refer to one of several similar modules. The specific module that is
- loaded for a virtual name is selected based on the current platform or
- environment.
- For example, packages are managed across platforms using the ``pkg`` module.
- ``pkg`` is a virtual module name that is an alias for the specific package
- manager module that is loaded on a specific system (for example, :mod:`yumpkg
- <salt.modules.yumpkg>` on RHEL/CentOS systems , and :mod:`aptpkg
- <salt.modules.aptpkg>` on Ubuntu).
- Virtual module names are set using the ``__virtual__`` function and the
- :ref:`virtual name <modules-virtual-name>`.
- ``__virtual__`` Function
- ========================
- The ``__virtual__`` function returns either a :ref:`string <python:typesseq>`,
- :py:data:`True`, :py:data:`False`, or :py:data:`False` with an :ref:`error
- string <modules-error-info>`. If a string is returned then the module is loaded
- using the name of the string as the virtual name. If ``True`` is returned the
- module is loaded using the current module name. If ``False`` is returned the
- module is not loaded. ``False`` lets the module perform system checks and
- prevent loading if dependencies are not met.
- Since ``__virtual__`` is called before the module is loaded, ``__salt__`` will
- be unreliable as not all modules will be available at this point in time. The
- ``__pillar__`` and ``__grains__`` :ref:`"dunder" dictionaries <dunder-dictionaries>`
- are available however.
- .. note::
- Modules which return a string from ``__virtual__`` that is already used by
- a module that ships with Salt will _override_ the stock module.
- .. _modules-error-info:
- Returning Error Information from ``__virtual__``
- ------------------------------------------------
- Optionally, Salt plugin modules, such as execution, state, returner, beacon,
- etc. modules may additionally return a string containing the reason that a
- module could not be loaded. For example, an execution module called ``cheese``
- and a corresponding state module also called ``cheese``, both depending on a
- utility called ``enzymes`` should have ``__virtual__`` functions that handle
- the case when the dependency is unavailable.
- .. code-block:: python
- """
- Cheese execution (or returner/beacon/etc.) module
- """
- try:
- import enzymes
- HAS_ENZYMES = True
- except ImportError:
- HAS_ENZYMES = False
- def __virtual__():
- """
- only load cheese if enzymes are available
- """
- if HAS_ENZYMES:
- return "cheese"
- else:
- return (
- False,
- "The cheese execution module cannot be loaded: enzymes unavailable.",
- )
- def slice():
- pass
- .. code-block:: python
- """
- Cheese state module. Note that this works in state modules because it is
- guaranteed that execution modules are loaded first
- """
- def __virtual__():
- """
- only load cheese if enzymes are available
- """
- # predicate loading of the cheese state on the corresponding execution module
- if "cheese.slice" in __salt__:
- return "cheese"
- else:
- return False, "The cheese state module cannot be loaded: enzymes unavailable."
- Examples
- --------
- The package manager modules are among the best examples of using the
- ``__virtual__`` function. A table of all the virtual ``pkg`` modules can be
- found :ref:`here <virtual-pkg>`.
- .. _module-provider-override:
- Overriding Virtual Module Providers
- -----------------------------------
- Salt often uses OS grains (``os``, ``osrelease``, ``os_family``, etc.) to
- determine which module should be loaded as the virtual module for ``pkg``,
- ``service``, etc. Sometimes this OS detection is incomplete, with new distros
- popping up, existing distros changing init systems, etc. The virtual modules
- likely to be affected by this are in the list below (click each item for more
- information):
- - :ref:`pkg <virtual-pkg>`
- - :ref:`service <virtual-service>`
- - :ref:`user <virtual-user>`
- - :ref:`shadow <virtual-shadow>`
- - :ref:`group <virtual-group>`
- If Salt is using the wrong module for one of these, first of all, please
- `report it on the issue tracker`__, so that this issue can be resolved for a
- future release. To make it easier to troubleshoot, please also provide the
- :py:mod:`grains.items <salt.modules.grains.items>` output, taking care to
- redact any sensitive information.
- Then, while waiting for the SaltStack development team to fix the issue, Salt
- can be made to use the correct module using the :conf_minion:`providers` option
- in the minion config file:
- .. code-block:: yaml
- providers:
- service: systemd
- pkg: aptpkg
- The above example will force the minion to use the :py:mod:`systemd
- <salt.modules.systemd>` module to provide service management, and the
- :py:mod:`aptpkg <salt.modules.aptpkg>` module to provide package management.
- .. __: https://github.com/saltstack/salt/issues/new
- Logging Restrictions
- --------------------
- As a rule, logging should not be done anywhere in a Salt module before it is
- loaded. This rule apples to all code that would run before the ``__virtual__()``
- function, as well as the code within the ``__virtual__()`` function itself.
- If logging statements are made before the virtual function determines if
- the module should be loaded, then those logging statements will be called
- repeatedly. This clutters up log files unnecessarily.
- Exceptions may be considered for logging statements made at the ``trace`` level.
- However, it is better to provide the necessary information by another means.
- One method is to :ref:`return error information <modules-error-info>` in the
- ``__virtual__()`` function.
- .. _modules-virtual-name:
- ``__virtualname__``
- ===================
- ``__virtualname__`` is a variable that is used by the documentation build
- system to know the virtual name of a module without calling the ``__virtual__``
- function. Modules that return a string from the ``__virtual__`` function
- must also set the ``__virtualname__`` variable.
- To avoid setting the virtual name string twice, you can implement
- ``__virtual__`` to return the value set for ``__virtualname__`` using a pattern
- similar to the following:
- .. code-block:: python
- # Define the module's virtual name
- __virtualname__ = "pkg"
- def __virtual__():
- """
- Confine this module to Mac OS with Homebrew.
- """
- if salt.utils.path.which("brew") and __grains__["os"] == "MacOS":
- return __virtualname__
- return False
- The ``__virtual__()`` function can return a ``True`` or ``False`` boolean, a tuple,
- or a string. If it returns a ``True`` value, this ``__virtualname__`` module-level
- attribute can be set as seen in the above example. This is the string that the module
- should be referred to as.
- When ``__virtual__()`` returns a tuple, the first item should be a boolean and the
- second should be a string. This is typically done when the module should not load. The
- first value of the tuple is ``False`` and the second is the error message to display
- for why the module did not load.
- For example:
- .. code-block:: python
- def __virtual__():
- """
- Only load if git exists on the system
- """
- if salt.utils.path.which("git") is None:
- return (False, "The git execution module cannot be loaded: git unavailable.")
- else:
- return True
- Documentation
- =============
- Salt execution modules are documented. The :func:`sys.doc` function will return
- the documentation for all available modules:
- .. code-block:: bash
- salt '*' sys.doc
- The ``sys.doc`` function simply prints out the docstrings found in the modules;
- when writing Salt execution modules, please follow the formatting conventions
- for docstrings as they appear in the other modules.
- Adding Documentation to Salt Modules
- ------------------------------------
- It is strongly suggested that all Salt modules have documentation added.
- To add documentation add a `Python docstring`_ to the function.
- .. code-block:: python
- def spam(eggs):
- """
- A function to make some spam with eggs!
- CLI Example::
- salt '*' test.spam eggs
- """
- return eggs
- Now when the sys.doc call is executed the docstring will be cleanly returned
- to the calling terminal.
- .. _`Python docstring`: https://docs.python.org/3/glossary.html#term-docstring
- Documentation added to execution modules in docstrings will automatically be
- added to the online web-based documentation.
- Add Execution Module Metadata
- -----------------------------
- When writing a Python docstring for an execution module, add information about
- the module using the following field lists:
- .. code-block:: text
- :maintainer: Thomas Hatch <thatch@saltstack.com, Seth House <shouse@saltstack.com>
- :maturity: new
- :depends: python-mysqldb
- :platform: all
- The maintainer field is a comma-delimited list of developers who help maintain
- this module.
- The maturity field indicates the level of quality and testing for this module.
- Standard labels will be determined.
- The depends field is a comma-delimited list of modules that this module depends
- on.
- The platform field is a comma-delimited list of platforms that this module is
- known to run on.
- Log Output
- ==========
- You can call the logger from custom modules to write messages to the minion
- logs. The following code snippet demonstrates writing log messages:
- .. code-block:: python
- import logging
- log = logging.getLogger(__name__)
- log.info("Here is Some Information")
- log.warning("You Should Not Do That")
- log.error("It Is Busted")
- Aliasing Functions
- ==================
- Sometimes one wishes to use a function name that would shadow a python built-in.
- A common example would be ``set()``. To support this, append an underscore to
- the function definition, ``def set_():``, and use the ``__func_alias__`` feature
- to provide an alias to the function.
- ``__func_alias__`` is a dictionary where each key is the name of a function in
- the module, and each value is a string representing the alias for that function.
- When calling an aliased function from a different execution module, state
- module, or from the cli, the alias name should be used.
- .. code-block:: python
- __func_alias__ = {
- "set_": "set",
- "list_": "list",
- }
- Private Functions
- =================
- In Salt, Python callable objects contained within an execution module are made
- available to the Salt minion for use. The only exception to this rule is a
- callable object with a name starting with an underscore ``_``.
- Objects Loaded Into the Salt Minion
- -----------------------------------
- .. code-block:: python
- def foo(bar):
- return bar
- Objects NOT Loaded into the Salt Minion
- ---------------------------------------
- .. code-block:: python
- def _foobar(baz): # Preceded with an _
- return baz
- cheese = {} # Not a callable Python object
- Useful Decorators for Modules
- =============================
- Depends Decorator
- -----------------
- When writing execution modules there are many times where some of the module
- will work on all hosts but some functions have an external dependency, such as
- a service that needs to be installed or a binary that needs to be present on
- the system.
- Instead of trying to wrap much of the code in large try/except blocks, a
- decorator can be used.
- If the dependencies passed to the decorator don't exist, then the salt minion
- will remove those functions from the module on that host.
- If a ``fallback_function`` is defined, it will replace the function instead of
- removing it
- .. code-block:: python
- import logging
- from salt.utils.decorators import depends
- log = logging.getLogger(__name__)
- try:
- import dependency_that_sometimes_exists
- except ImportError as e:
- log.trace("Failed to import dependency_that_sometimes_exists: {0}".format(e))
- @depends("dependency_that_sometimes_exists")
- def foo():
- """
- Function with a dependency on the "dependency_that_sometimes_exists" module,
- if the "dependency_that_sometimes_exists" is missing this function will not exist
- """
- return True
- def _fallback():
- """
- Fallback function for the depends decorator to replace a function with
- """
- return '"dependency_that_sometimes_exists" needs to be installed for this function to exist'
- @depends("dependency_that_sometimes_exists", fallback_function=_fallback)
- def foo():
- """
- Function with a dependency on the "dependency_that_sometimes_exists" module.
- If the "dependency_that_sometimes_exists" is missing this function will be
- replaced with "_fallback"
- """
- return True
- In addition to global dependencies the depends decorator also supports raw
- booleans.
- .. code-block:: python
- from salt.utils.decorators import depends
- HAS_DEP = False
- try:
- import dependency_that_sometimes_exists
- HAS_DEP = True
- except ImportError:
- pass
- @depends(HAS_DEP)
- def foo():
- return True
|