开发插件

插件通过逻辑和功能增强了 Ansible 的核心功能,所有模块都可以访问这些功能。Ansible 集合包含许多方便的插件,您可以轻松编写自己的插件。所有插件必须

  • 用 Python 编写

  • 引发错误

  • 以 Unicode 形式返回字符串

  • 符合 Ansible 的配置和文档标准

在您查看了这些一般准则后,您可以跳到要开发的特定类型的插件。

用 Python 编写插件

您必须用 Python 编写插件,以便 PluginLoader 可以加载它并将其作为任何模块都可以使用的 Python 对象返回。由于您的插件将在控制节点上执行,因此您必须使用 兼容版本的 Python 编写它。

引发错误

您应该通过引发 AnsibleError() 或具有描述错误消息的类似类来返回插件执行期间遇到的错误。当将其他异常包装到错误消息中时,您应该始终使用 to_native Ansible 函数来确保跨 Python 版本的正确字符串兼容性

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

由于 Ansible 仅在需要时才评估变量,因此过滤器和测试插件应传播异常 jinja2.exceptions.UndefinedErrorAnsibleUndefinedVariable 以确保未定义的变量仅在必要时才为致命错误。

检查不同的 AnsibleError 对象 并查看哪个最适合您的情况。检查您正在开发的特定插件类型的部分以获取特定于类型的错误处理详细信息。

字符串编码

您必须将插件返回的任何字符串转换为 Python 的 Unicode 类型。转换为 Unicode 可确保这些字符串可以通过 Jinja2 运行。要转换字符串

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

插件配置和文档标准

要为您的插件定义可配置选项,请在 Python 文件的 DOCUMENTATION 部分中描述它们。回调和连接插件已通过这种方式声明了配置要求,自 Ansible 2.4 版本以来;现在大多数插件类型也这样做。这种方法确保了插件选项的文档始终正确且是最新的。要向插件添加可配置选项,请以以下格式定义它

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

要访问插件中的配置设置,请使用 self.get_option(<option_name>)。某些插件类型对此的处理方式有所不同

  • Become、回调、连接和 shell 插件保证引擎会调用 set_options()

  • 查找插件始终要求您在 run() 方法中处理它。

  • 如果使用 base _read_config_file() 方法,则会自动完成清单插件。如果不是,则必须使用 self.get_option(<option_name>)

  • 缓存插件在加载时执行此操作。

  • Cliconf、httpapi 和 netconf 插件间接地依赖于连接插件。

  • 首次访问变量插件设置时(使用 self.get_option()self.get_options() 方法)会填充这些设置。

如果您需要显式填充设置,请使用 self.set_options() 调用。

配置源遵循 Ansible 中值的优先级规则。当同一类别有多个值时,最后定义的值优先。例如,在上面的配置块中,如果同时定义了 name_of_ansible_varname_of_second_var,则 option_name 选项的值将是 name_of_second_var 的值。有关更多信息,请参阅 控制 Ansible 的行为:优先级规则

支持嵌入文档的插件(请参阅 ansible-doc 获取列表)应包含格式良好的文档字符串。如果您继承自插件,则必须记录它接受的选项,方法是通过文档片段或副本。有关正确文档的更多信息,请参阅 模块格式和文档。即使您正在开发供本地使用的插件,彻底的文档也是一个好主意。

在 ansible-core 2.14 中,我们添加了对记录过滤器和测试插件的支持。您可以通过两种方式提供文档
  • 定义一个 Python 文件,其中包含每个插件的内联文档。

  • 为多个插件定义一个 Python 文件,并以 YAML 格式创建相邻的文档文件。

开发特定类型的插件

操作插件

操作插件允许您将本地处理和本地数据与模块功能集成。

要创建操作插件,请创建一个新类,并使用 Base(ActionBase) 类作为父类

from ansible.plugins.action import ActionBase

class ActionModule(ActionBase):
    pass

从那里,使用 _execute_module 方法执行模块以调用原始模块。在模块成功执行后,您可以修改模块返回数据。

module_return = self._execute_module(module_name='<NAME_OF_MODULE>',
                                     module_args=module_args,
                                     task_vars=task_vars, tmp=tmp)

例如,如果您想检查 Ansible 控制节点和目标机器之间的时间差,您可以编写一个操作插件来检查本地时间并将其与 Ansible 的 setup 模块的返回数据进行比较

#!/usr/bin/python
# Make coding more python3-ish, this is required for contributions to Ansible
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

from ansible.plugins.action import ActionBase
from datetime import datetime


class ActionModule(ActionBase):
    def run(self, tmp=None, task_vars=None):
        super(ActionModule, self).run(tmp, task_vars)
        module_args = self._task.args.copy()
        module_return = self._execute_module(module_name='setup',
                                             module_args=module_args,
                                             task_vars=task_vars, tmp=tmp)
        ret = dict()
        remote_date = None
        if not module_return.get('failed'):
            for key, value in module_return['ansible_facts'].items():
                if key == 'ansible_date_time':
                    remote_date = value['iso8601']

        if remote_date:
            remote_date_obj = datetime.strptime(remote_date, '%Y-%m-%dT%H:%M:%SZ')
            time_delta = datetime.utcnow() - remote_date_obj
            ret['delta_seconds'] = time_delta.seconds
            ret['delta_days'] = time_delta.days
            ret['delta_microseconds'] = time_delta.microseconds

        return dict(ansible_facts=dict(ret))

此代码检查控制节点上的时间,使用 setup 模块捕获远程机器的日期和时间,并计算捕获时间和本地时间之间的差值,以天、秒和微秒为单位返回时间差。

有关操作插件的实际示例,请参阅 Ansible 核心附带的操作插件的源代码

缓存插件

缓存插件存储清单插件检索到的收集的事实和数据。

使用 cache_loader 导入缓存插件,以便您可以使用 self.set_options()self.get_option(<option_name>)。如果直接在代码库中导入缓存插件,则只能通过 ansible.constants 访问选项,并且会破坏缓存插件被清单插件使用的能力。

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

缓存插件有两个基类,BaseCacheModule 用于基于数据库的缓存,以及 BaseCacheFileModule 用于基于文件的缓存。

要创建缓存插件,首先创建一个新的 CacheModule 类,并使用合适的基类。如果使用 __init__ 方法创建插件,则应使用任何提供的参数(args 和 kwargs)初始化基类,以与清单插件缓存选项兼容。基类调用 self.set_options(direct=kwargs)。在调用基类 __init__ 方法之后,应使用 self.get_option(<option_name>) 来访问缓存选项。

新的缓存插件应使用选项 _uri_prefix_timeout,以与现有的缓存插件保持一致。

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')

如果使用 BaseCacheModule,则必须实现 getcontainskeyssetdeleteflushcopy 方法。 contains 方法应返回一个布尔值,指示键是否存在且未过期。与基于文件的缓存不同,如果缓存已过期,get 方法不会引发 KeyError。

如果使用 BaseFileCacheModule,则必须实现 _load_dump 方法,这些方法将从基类方法 getset 中调用。

如果缓存插件存储 JSON,则在 _dumpset 方法中使用 AnsibleJSONEncoder,并在 _loadget 方法中使用 AnsibleJSONDecoder

有关示例缓存插件,请参阅 Ansible Core 附带的 缓存插件源代码

回调插件

回调插件在响应事件时为 Ansible 添加新的行为。默认情况下,回调插件控制在运行命令行程序时看到的大部分输出。

要创建回调插件,请创建一个新的类,并将 Base(Callbacks) 类作为父类。

from ansible.plugins.callback import CallbackBase

class CallbackModule(CallbackBase):
    pass

从那里,覆盖 CallbackBase 中您想要提供回调的特定方法。对于打算与 Ansible 2.0 及更高版本一起使用的插件,您应该只覆盖以 v2 开头的类方法。有关可以覆盖的方法的完整列表,请参阅 lib/ansible/plugins/callback 目录中的 __init__.py

以下是 Ansible 的计时器插件实现的修改示例,但添加了一个额外的选项,以便您可以了解 Ansible 2.4 及更高版本中的配置方式。

# Make coding more python3-ish, this is required for contributions to Ansible
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

# not only visible to ansible-doc, it also 'declares' the options the plugin requires and how to configure them.
DOCUMENTATION = '''
name: timer
callback_type: aggregate
requirements:
    - enable in configuration
short_description: Adds time to play stats
version_added: "2.0"  # for collections, use the collection version, not the Ansible version
description:
    - This callback just adds total play duration to the play stats.
options:
  format_string:
    description: format of the string shown to user at play end
    ini:
      - section: callback_timer
        key: format_string
    env:
      - name: ANSIBLE_CALLBACK_TIMER_FORMAT
    default: "Playbook run took %s days, %s hours, %s minutes, %s seconds"
'''
from datetime import datetime

from ansible.plugins.callback import CallbackBase


class CallbackModule(CallbackBase):
    """
    This callback module tells you how long your plays ran for.
    """
    CALLBACK_VERSION = 2.0
    CALLBACK_TYPE = 'aggregate'
    CALLBACK_NAME = 'namespace.collection_name.timer'

    # only needed if you ship it and don't want to enable by default
    CALLBACK_NEEDS_ENABLED = True

    def __init__(self):

      # make sure the expected objects are present, calling the base's __init__
      super(CallbackModule, self).__init__()

      # start the timer when the plugin is loaded, the first play should start a few milliseconds after.
      self.start_time = datetime.now()

    def _days_hours_minutes_seconds(self, runtime):
      ''' internal helper method for this callback '''
      minutes = (runtime.seconds // 60) % 60
      r_seconds = runtime.seconds - (minutes * 60)
      return runtime.days, runtime.seconds // 3600, minutes, r_seconds

    # this is only event we care about for display, when the play shows its summary stats; the rest are ignored by the base class
    def v2_playbook_on_stats(self, stats):
      end_time = datetime.now()
      runtime = end_time - self.start_time

      # Shows the usage of a config option declared in the DOCUMENTATION variable. Ansible will have set it when it loads the plugin.
      # Also note the use of the display object to print to screen. This is available to all callbacks, and you should use this over printing yourself
      self._display.display(self._plugin_options['format_string'] % (self._days_hours_minutes_seconds(runtime)))

请注意,CALLBACK_VERSIONCALLBACK_NAME 定义对于 Ansible 2.0 及更高版本中正常运行的插件是必需的。CALLBACK_TYPE 主要用于区分“stdout”插件和其他插件,因为您只能加载一个写入 stdout 的插件。

有关示例回调插件,请参阅 Ansible Core 附带的 回调插件源代码

ansible-core 2.11 中的新增功能,回调插件会收到 任务的通知(由 v2_playbook_on_task_start)。默认情况下,只有用户在剧本中列出的显式 meta 任务才会发送到回调。

还有一些任务是在执行过程中的不同点内部隐式生成的。回调插件也可以选择接收这些隐式任务,方法是设置 self.wants_implicit_tasks = True。回调钩子接收到的任何 Task 对象都将具有一个 .implicit 属性,可以查询该属性以确定 Task 是来自 Ansible 内部还是由用户显式指定。

连接插件

连接插件允许 Ansible 连接到目标主机,以便在目标主机上执行任务。Ansible 附带了许多连接插件,但每次只能对每个主机使用一个。最常用的连接插件是本机 sshparamikolocal。所有这些都可以与临时任务和剧本一起使用。

要创建新的连接插件(例如,支持 SNMP、消息总线或其他传输),请复制现有连接插件之一的格式,并将其放入 本地插件路径 上的 connection 目录中。

连接插件可以通过在属性名称(在本例中为 timeout)的文档中定义一个条目来支持通用选项(例如 --timeout 标志)。如果通用选项具有非空默认值,则插件应定义相同的默认值,因为不同的默认值将被忽略。

有关示例连接插件,请参阅 Ansible Core 附带的 连接插件源代码

过滤器插件

过滤器插件用于操作数据。它们是 Jinja2 的一项功能,也可用于 template 模块使用的 Jinja2 模板。与所有插件一样,它们可以轻松扩展,但您可以每个文件包含多个插件,而不是每个插件一个文件。Ansible 附带的大多数过滤器插件都位于 core.py 中。

过滤器插件不使用上面描述的标准配置系统,但从 ansible-core 2.14 开始可以使用它作为普通文档。

由于 Ansible 仅在需要时才评估变量,因此过滤器插件应传播异常 jinja2.exceptions.UndefinedErrorAnsibleUndefinedVariable,以确保未定义的变量仅在必要时才会导致致命错误。

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

有关示例过滤器插件,请参阅 Ansible Core 附带的 过滤器插件源代码

清单插件

清单插件解析清单源并形成清单的内存表示。清单插件是在 Ansible 2.4 版本中添加的。

您可以在 开发动态清单 页面中查看清单插件的详细信息。

查找插件

查找插件从外部数据存储中提取数据。查找插件可以在剧本中用于循环——剧本语言结构(如 with_fileglobwith_items)是通过查找插件实现的——以及将值返回到变量或参数中。

即使只有一个元素,查找插件也应返回列表。

Ansible 包含许多 过滤器,可用于操作查找插件返回的数据。有时在查找插件内部进行过滤是有意义的,而其他时候最好返回可以在剧本中过滤的结果。在确定要在查找插件内部执行的适当过滤级别时,请记住如何引用数据。

这是一个简单的查找插件实现——此查找将文本文件的内容作为变量返回。

# python 3 headers, required if submitting to Ansible
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

DOCUMENTATION = r"""
  name: file
  author: Daniel Hokka Zakrisson (@dhozac) <[email protected]>
  version_added: "0.9"  # for collections, use the collection version, not the Ansible version
  short_description: read file contents
  description:
      - This lookup returns the contents from a file on the Ansible control node's file system.
  options:
    _terms:
      description: path(s) of files to read
      required: True
    option1:
      description:
            - Sample option that could modify plugin behavior.
            - This one can be set directly ``option1='x'`` or in ansible.cfg, but can also use vars or environment.
      type: string
      ini:
        - section: file_lookup
          key: option1
  notes:
    - if read in variable context, the file can be interpreted as YAML if the content is valid to the parser.
    - this lookup does not understand globbing --- use the fileglob lookup instead.
"""
from ansible.errors import AnsibleError, AnsibleParserError
from ansible.plugins.lookup import LookupBase
from ansible.utils.display import Display

display = Display()

class LookupModule(LookupBase):

    def run(self, terms, variables=None, **kwargs):

      # First of all populate options,
      # this will already take into account env vars and ini config
      self.set_options(var_options=variables, direct=kwargs)

      # lookups in general are expected to both take a list as input and output a list
      # this is done so they work with the looping construct 'with_'.
      ret = []
      for term in terms:
          display.debug("File lookup term: %s" % term)

          # Find the file in the expected search path, using a class method
          # that implements the 'expected' search path for Ansible plugins.
          lookupfile = self.find_file_in_search_path(variables, 'files', term)

          # Don't use print or your own logging, the display class
          # takes care of it in a unified way.
          display.vvvv(u"File lookup using %s as file" % lookupfile)
          try:
              if lookupfile:
                  contents, show_data = self._loader._get_file_contents(lookupfile)
                  ret.append(contents.rstrip())
              else:
                  # Always use ansible error classes to throw 'final' exceptions,
                  # so the Ansible engine will know how to deal with them.
                  # The Parser error indicates invalid options passed
                  raise AnsibleParserError()
          except AnsibleParserError:
              raise AnsibleError("could not locate file in lookup: %s" % term)

          # consume an option: if this did something useful, you can retrieve the option value here
          if self.get_option('option1') == 'do something':
            pass

      return ret

以下是此查找的调用示例。

---
- hosts: all
  vars:
     contents: "{{ lookup('namespace.collection_name.file', '/etc/foo.txt') }}"
     contents_with_option: "{{ lookup('namespace.collection_name.file', '/etc/foo.txt', option1='donothing') }}"
  tasks:

     - debug:
         msg: the value of foo.txt is {{ contents }} as seen today {{ lookup('pipe', 'date +"%Y-%m-%d"') }}

有关示例查找插件,请参阅 Ansible Core 附带的 查找插件源代码

有关查找插件的更多使用示例,请参阅 使用查找

测试插件

测试插件用于验证数据。它们是 Jinja2 的一项功能,也可用于 template 模块使用的 Jinja2 模板。与所有插件一样,它们可以轻松扩展,但您可以每个文件包含多个插件,而不是每个插件一个文件。Ansible 附带的大多数测试插件都位于 core.py 中。这些与某些过滤器插件(如 mapselect)结合使用特别有用;它们也可用于条件指令(如 when:)。

测试插件不使用上面描述的标准配置系统。从 ansible-core 2.14 开始,测试插件可以使用普通文档。

由于 Ansible 仅在需要时才评估变量,因此测试插件应传播异常 jinja2.exceptions.UndefinedErrorAnsibleUndefinedVariable,以确保未定义的变量仅在必要时才会导致致命错误。

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

有关示例测试插件,请参阅 Ansible Core 附带的 测试插件源代码

变量插件

变量插件将额外的变量数据注入到 Ansible 运行中,这些数据不是来自清单源、剧本或命令行。剧本结构(如“host_vars”和“group_vars”)使用变量插件工作。

变量插件在 Ansible 2.0 中已部分实现,并在 Ansible 2.4 开始重写为完全实现。从 Ansible 2.10 开始,集合支持变量插件。

旧版插件使用 run 方法作为其主体/工作。

def run(self, name, vault_password=None):
    pass # your code goes here

Ansible 2.0 不会将密码传递给旧版插件,因此金库不可用。现在,大部分工作都在 get_vars 方法中进行,该方法在需要时由 VariableManager 调用。

def get_vars(self, loader, path, entities):
    pass # your code goes here

参数如下:

  • loader:Ansible 的数据加载器。数据加载器可以读取文件、自动加载 JSON/YAML 和解密保管库数据,以及缓存读取的文件。

  • path:这是每个清单源和当前 playbook 目录的“目录数据”,以便它们可以根据这些数据搜索数据。get_vars 至少会为每个可用的路径调用一次。

  • entities:这些是与所需变量相关的主机或组名称。插件将为主机调用一次,为组调用另一次。

get_vars 方法只需要返回一个包含变量的字典结构。

从 Ansible 2.4 版本开始,vars 插件仅在准备执行任务时按需执行。这避免了在较旧版本的 Ansible 中构建清单期间发生的代价高昂的“始终执行”行为。从 Ansible 2.10 版本开始,用户可以通过切换 vars 插件的执行,使其在准备执行任务时运行或在导入清单源后运行。

用户必须显式启用位于集合中的 vars 插件。有关详细信息,请参阅 启用 vars 插件

传统 vars 插件默认情况下始终加载并运行。您可以通过将 REQUIRES_ENABLED 设置为 True 来阻止它们自动运行。

class VarsModule(BaseVarsPlugin):
    REQUIRES_ENABLED = True

包含 vars_plugin_staging 文档片段,以允许用户确定 vars 插件何时运行。

DOCUMENTATION = '''
    name: custom_hostvars
    version_added: "2.10"  # for collections, use the collection version, not the Ansible version
    short_description: Load custom host vars
    description: Load custom host vars
    options:
      stage:
        ini:
          - key: stage
            section: vars_custom_hostvars
        env:
          - name: ANSIBLE_VARS_PLUGIN_STAGE
    extends_documentation_fragment:
      - vars_plugin_staging
'''

有时,vars 插件提供的值将包含不安全的值。应使用 ansible.utils.unsafe_proxy 提供的实用程序函数 wrap_var 来确保 Ansible 正确处理变量和值。不安全数据的用例在 不安全或原始字符串 中进行了介绍。

from ansible.plugins.vars import BaseVarsPlugin
from ansible.utils.unsafe_proxy import wrap_var

class VarsPlugin(BaseVarsPlugin):
    def get_vars(self, loader, path, entities):
        return dict(
            something_unsafe=wrap_var("{{ SOMETHING_UNSAFE }}")
        )

例如,有关 vars 插件,请参阅 Ansible Core 中包含的 vars 插件 的源代码。

另请参阅

集合索引

浏览现有的集合、模块和插件

Python API

了解任务执行的 Python API

开发动态清单

了解如何开发动态清单源

开发模块

了解如何编写 Ansible 模块

沟通

有问题?需要帮助?想分享你的想法?请访问 Ansible 沟通指南

相邻的 YAML 文档文件

备用 YAML 文件作为文档