开发插件
插件通过逻辑和功能增强了 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.UndefinedError 和 AnsibleUndefinedVariable 异常,以确保仅在必要时才将未定义变量视为致命错误。
请检查不同的 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_var 和 name_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, flush 和 copy 方法。contains 方法应返回一个布尔值,指示键是否存在且未过期。与基于文件的缓存不同,如果缓存已过期,get 方法不会引发 KeyError。
如果使用 BaseFileCacheModule,则必须实现将由基类方法 get 和 set 调用的 _load 和 _dump 方法。
如果你的缓存插件存储 JSON,请在 _dump 或 set 方法中使用 AnsibleJSONEncoder,并在 _load 或 get 方法中使用 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_VERSION 和 CALLBACK_NAME。CALLBACK_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, paramiko 和 local。所有这些插件都可以在 ad-hoc 任务和 Playbook 中使用。
要创建新的连接插件(例如,支持 SNMP、消息总线或其他传输),请复制现有连接插件之一的格式,并将其放入 本地插件路径 的 connection 目录中。
连接插件可以通过在文档中为属性名称(本例中为 timeout)定义条目来支持常用选项(如 --timeout 标志)。如果通用选项具有非空默认值,插件应定义相同的默认值,因为不同的默认值将被忽略。
有关连接插件的示例,请参阅 Ansible Core 随附的连接插件 源代码。
过滤器插件
过滤器插件用于操作数据。它们是 Jinja2 的一项功能,也可在 template 模块使用的 Jinja2 模板中使用。与所有插件一样,它们可以轻松扩展,但不必为每个插件创建一个文件,可以在每个文件中包含多个插件。Ansible 随附的大多数过滤器插件驻留在 core.py 中。
过滤器插件不使用上述标准配置系统,但自 ansible-core 2.14 起可以将其用作纯文档。
由于 Ansible 仅在需要时才对变量进行求值,因此过滤器插件应传播 jinja2.exceptions.UndefinedError 和 AnsibleUndefinedVariable 异常,以确保仅在必要时才将未定义变量视为致命错误。
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_fileglob 和 with_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 中。这些插件在配合某些过滤器插件(如 map 和 select)使用时特别有用;它们也可用于条件指令,如 when:。
测试插件不使用上述标准配置系统。自 ansible-core 2.14 起,测试插件可以使用纯文档。
由于 Ansible 仅在需要时才对变量进行求值,因此测试插件应传播 jinja2.exceptions.UndefinedError 和 AnsibleUndefinedVariable 异常,以确保仅在必要时才将未定义变量视为致命错误。
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 文件作为文档