开发插件

插件通过逻辑和功能增强 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 Core 中包含的动作插件的源代码

缓存插件

缓存插件存储通过清单插件收集的事实和数据。

使用 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 中的新增功能是,回调插件会收到 meta 任务的通知(通过 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 的 DataLoader。DataLoader 可以读取文件、自动加载 JSON/YAML 并解密保管库数据,以及缓存读取的文件。

  • path: 这是每个清单源和当前剧本的剧本目录的 ‘directory data’,以便它们可以根据这些目录搜索数据。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 插件提供的 value 会包含不安全的 value。应该使用 ansible.utils.unsafe_proxy 提供的实用程序函数 wrap_var 来确保 Ansible 正确处理变量和 value。不安全数据的用例在 不安全或原始字符串 中介绍。

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 文件作为文档