开发插件

插件通过逻辑和功能增强 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 格式的文档文件。

开发特定类型的插件

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 核心包含的 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,则必须实现以下方法:getcontainskeyssetdeleteflushcopycontains 方法应返回一个布尔值,指示键是否存在且未过期。与基于文件缓存不同,如果缓存已过期,get 方法不会引发 KeyError。

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

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

有关示例缓存插件,请参阅 Ansible 核心包含的缓存插件源代码

回调插件

回调插件在响应事件时为 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_NAME 定义。CALLBACK_TYPE 主要用于区分“stdout”插件和其他插件,因为您只能加载一个写入 stdout 的插件。

有关示例回调插件,请参阅 Ansible 核心包含的回调插件源代码

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

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

连接插件

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

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

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

有关示例连接插件,请参阅 Ansible 核心包含的连接插件源代码

过滤器插件

过滤器插件用于操作数据。它们是 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 核心包含的过滤器插件源代码

清单插件

清单插件解析清单源并形成清单的内存表示。清单插件是在 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 核心包含的查找插件源代码

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

测试插件

测试插件用于验证数据。它们是 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:这是每个清单源和当前剧本的剧本目录的“目录数据”,因此它们可以参考这些目录搜索数据。get_vars方法将至少为每个可用的路径调用一次。

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

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

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

用户必须显式启用位于集合中的变量插件。详情请参见启用变量插件

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

class VarsModule(BaseVarsPlugin):
    REQUIRES_ENABLED = True

包含vars_plugin_staging文档片段,以允许用户确定变量插件的运行时间。

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

有时,变量插件提供的值将包含不安全的值。应使用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 }}")
        )

例如变量插件,请参阅Ansible Core包含的变量插件源代码

另请参阅

集合索引

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

Python API

了解任务执行的Python API

开发动态清单

了解如何开发动态清单源

开发模块

了解如何编写Ansible模块

沟通

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

相邻的 YAML 文档文件

备选YAML文件作为文档