开发插件

插件通过逻辑和功能增强 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__ 方法创建插件,则应使用任何提供的参数和关键字参数初始化基类,以与清单插件缓存选项兼容。基类调用 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,则必须实现方法 getcontainskeyssetdeleteflushcopycontains 方法应返回一个布尔值,指示键是否存在且未过期。与基于文件的文件不同,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) <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 提供的 测试插件的源代码

变量插件

变量插件将额外的变量数据注入到 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 和解密保管库数据,以及缓存读取文件。

  • 路径: 这是每个清单源和当前剧本的剧本目录的“目录数据”,以便它们可以参考这些数据进行搜索。 get_vars 方法将在每个可用路径至少被调用一次。

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

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