Writing Execution Modules

Salt execution modules are the functions called by the salt command.

模块也很容易写!

Writing Salt execution modules is straightforward.

A Salt execution module is a Python or Cython module placed in a directory called _modules/ within the file_roots as specified by the master config file. By default this is /srv/salt/_modules on Linux systems.

Modules placed in _modules/ will be synced to the minions when any of the following Salt functions are called:

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 __virtual__ function.

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.

如果要增加一个Cyphon模块,文件必须以``<模块名>.pyx``命名,这样调用者才知道模块需要作为一个Cython模块导入。Cython模块的编译是Minion启动时自动进行的,所以只需要``*.pyx``文件就可以了。

Zip Archives as Modules

Python 2.3 and higher allows developers to directly import zip archives containing Python code. By setting 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.

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.

# 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:

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 file_roots, Salt users can distribute and use lumberjack.zip like any other module.

$ sudo salt minion1 saltutil.sync_modules
minion1:
  - modules.lumberjack
$ sudo salt minion1 lumberjack.is_ok 'Michael Palin'
minion1:
  True

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 Python dictionary 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:

def foo(bar):
    return __salt__['cmd.run'](bar)

This code will call the run function in the cmd module and pass the argument bar to it.

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 数据

在Minion上,Salt Grains检测到的值是一个字典:ref:dict <python2:typesmapping>,字典命名为``__grains__``而且可以从Python 模块内部可调用对象进行访问。

To see the contents of the grains dictionary for a given system in your deployment run the grains.items() function:

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'].

模块配置

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. salt.modules.test, conf/minion.

输出配置

Since execution module functions can return different data, and the way the data is printed can greatly change the presentation, Salt has a printout configuration.

When writing a module the __outputter__ dictionary can be declared in the module. The __outputter__ dictionary contains a mapping of function name to Salt Outputter.

__outputter__ = {
                'run': 'txt'
                }

这确保文字输出器被使用了。

虚拟模块

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, yumpkg on RHEL/CentOS systems , and aptpkg on Ubuntu).

Virtual module names are set using the __virtual__ function and the virtual name.

__virtual__ Function

The __virtual__ function returns either a string, True, False, or False with an error string. 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 unavailable as it will not have been packed into the module at this point in time.

注解

Modules which return a string from __virtual__ that is already used by a module that ships with Salt will _override_ the stock module.

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.

'''
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.')
'''
Cheese state module
'''

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. Some examples:

__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:

# Define the module's virtual name
__virtualname__ = 'pkg'


def __virtual__():
    '''
    Confine this module to Mac OS with Homebrew.
    '''

    if salt.utils.which('brew') and __grains__['os'] == 'MacOS':
        return __virtualname__
    return False

文档

Salt execution modules are documented. The sys.doc() function will return the documentation for all available modules:

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.

给Salt模块增加文档

It is strongly suggested that all Salt modules have documentation added.

To add documentation add a Python docstring to the function.

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.

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:

: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:

import logging

log = logging.getLogger(__name__)

log.info('Here is Some Information')
log.warning('You Should Not Do That')
log.error('It Is Busted')

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 _.

Salt Minion中调用的对象

def foo(bar):
    return bar

class baz:
    def __init__(self, quo):
        pass

Salt Minion没有调用的对象

def _foobar(baz): # Preceded with an _
    return baz

cheese = {} # Not a callable Python object

注解

Some callable names also end with an underscore _, to avoid keyword clashes with Python keywords. When using execution modules, or state modules, with these in them the trailing underscore should be omitted.

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

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.

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