开发插件

插件通过逻辑和功能增强了 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: MYCOLLECTION_NAME_ENV_VAR_NAME
    ini:
      - section: mycollection_section_of_ansible.cfg_where_this_config_option_is_defined
        key: key_used_in_ansible.cfg
    vars:
      - name: mycollection_name_of_ansible_var
      - name: mycollection_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

支持的配置字段有:

env

可用于设置此选项的环境变量列表。每个条目都包含一个指定环境变量名称的 name 字段。名称应为大写,并应以集合名称为前缀。同一个选项可以列出多个环境变量。如果设置了多个环境变量,列表中最后设置的一个将优先。这通常用于插件(特别是清单插件),以允许通过环境变量进行配置。示例:VMWARE_PORT, GRAFANA_PASSWORD

ini

可用于设置此选项的配置文件设置列表。每个条目都包含一个用于配置文件部分的 section 字段,以及一个用于配置键的 key 字段。两者都应为小写,并应以集合名称为前缀。同一个选项可以列出多个配置设置。如果设置了多个配置,列表中最后设置的一个将优先。这允许通过 ansible.cfg 配置插件。示例:grafana_password

vars

可用于设置此选项的 Ansible 变量列表。每个条目都包含一个指定变量名称的 name 字段。名称应为小写,并应以集合名称为前缀。同一个选项可以列出多个变量。如果设置了多个变量,列表中最后设置的一个将优先。变量遵循 Ansible 的变量优先级规则。这允许通过 Ansible 变量配置插件。示例:ansible_vmware_port

通用优先级规则

配置源的优先级规则如下所示,按优先级从高到低排列:

  • 关键字

  • CLI 设置

  • 环境变量 (env)

  • ansible.cfg 中定义的数值

  • 选项的默认值(如果存在)。

访问配置设置

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

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

  • 查找插件始终要求你在 run() 方法中自行处理。

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

  • 缓存插件在加载时完成。

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

  • Vars 插件设置在首次访问时(使用 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 格式文档文件。

开发特定类型的插件

Action 插件

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

要创建 action 插件,请创建一个以 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 控制节点与目标机器之间的时间差,你可以编写一个 action 插件来检查本地时间,并将其与 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 模块获取远程机器的日期和时间,计算捕获的时间与本地时间之间的差值,并以天、秒和微秒为单位返回时间差。

有关 action 插件的实践示例,请参阅 Ansible Core 随附的 action 插件 源代码。

缓存插件

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

请使用 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,则必须实现 get, contains, keys, set, delete, flushcopy 方法。contains 方法应返回一个布尔值,指示键是否存在且未过期。与基于文件的缓存不同,如果缓存已过期,get 方法不会引发 KeyError。

如果使用 BaseFileCacheModule,则必须实现将由基类方法 getset 调用的 _load_dump 方法。

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

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

请注意,缓存插件实现是内部细节,不应依赖于外部用途(如在 Playbook 中进行查询或使用)。

假设在任何时间点产生的缓存都可以在未来的任何时间点使用,因为底层实现或其中提供的信息可能会发生变化。

如果缓存的计划用途是外部查询或使用,我们建议明确获取和存储数据,例如创建一个收集事实并将它们存储为你所需格式的 Playbook,然后将该数据明确存储在缓存概念之外,使其变得可靠。

回调插件

回调插件在响应事件时为 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)))

请注意,对于 Ansible 2.0 及更高版本,插件要正常工作,必须定义 CALLBACK_VERSIONCALLBACK_NAMECALLBACK_TYPE 主要用于将 ‘stdout’ 插件与其余插件区分开,因为你一次只能加载一个写入 stdout 的插件。

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

ansible-core 2.11 中新增功能:回调插件现在会收到 meta 任务的通知(通过 v2_playbook_on_task_start)。默认情况下,只有用户在 Play 中明确列出的 meta 任务才会发送给回调。

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

连接插件

连接插件允许 Ansible 连接到目标主机,以便在其上执行任务。Ansible 附带了许多连接插件,但每个主机一次只能使用一个。最常用的连接插件是原生的 ssh, paramikolocal。所有这些插件都可以在 ad-hoc 任务和 Playbook 中使用。

要创建新的连接插件(例如,支持 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 版本中添加的。

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

查找插件

查找插件从外部数据存储中提取数据。查找插件可在 Playbook 中用于循环(如 with_fileglobwith_items 等 Playbook 语言构造是通过查找插件实现的),也可用于将值返回到变量或参数中。

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

Ansible 包含许多可用于操作查找插件返回数据的 过滤器。有时在查找插件内部进行过滤是有意义的,而另一些时候在 Playbook 中过滤结果会更好。在确定查找插件内应执行的过滤级别时,请记住数据将如何被引用。

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

# 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) <daniel@hozac.com>
  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 随附的测试插件 源代码。

Vars 插件

Vars 插件将非来自清单源、Playbook 或命令行的额外变量数据注入到 Ansible 运行中。像 ‘host_vars’ 和 ‘group_vars’ 这样的 Playbook 构造就是使用 vars 插件工作的。

Vars 插件在 Ansible 2.0 中得到了部分实现,并从 Ansible 2.4 开始被重写为完全实现。自 Ansible 2.10 起,集合开始支持 vars 插件。

较旧的插件使用 run 方法作为其主体/工作入口。

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

Ansible 2.0 不会将密码传递给旧插件,因此无法使用保险箱(vault)。现在大部分工作发生在 get_vars 方法中,该方法在需要时由 VariableManager 调用。

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

参数为:

  • loader: Ansible 的 DataLoader。DataLoader 可以读取文件、自动加载 JSON/YAML、解密保险箱数据并缓存读取的文件。

  • path: 这是每个清单源和当前 Play 的 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 文件作为文档