开发模组
所谓模组,是 Ansible 代表咱们在本地或远程运行的可重复使用的独立脚本。模组会与咱们的本地机器、API 或远端系统交互,以执行像是更改数据库密码或启动云实例等特定任务。每个模组都可由 Ansible API、ansible
或 ansible-playbook
等程序使用。模组会提供一些预定义接口,会接受一些参数,并在退出前通过向 stdout
打印 JSON 字符串,向 Ansible 返回信息。
如果咱们需要的功能,在成千上万的 Ansible 模组集合中都不可用,咱们则可以轻松编写咱们自己的定制模组。在咱们编写某个用于本地用途的模组时,咱们可以选择任何编程语言,并遵循咱们自己的规则。请用这个主题,了解如何用 Python 创建 Ansible 模组。在咱们创建出某个模组后,必须在本地将其添加到相应的目录中,以便 Ansible 能找到并执行他。有关在本地添加某个模组的详情,请参阅 在本地添加模组和插件。
准备开发 Ansible 模组的环境
咱们只需安装 ansible-core
就可以测试模组。模组可以任何语言编写,但以下指南的大部分内容,都假定咱们使用的是 Python。纳入 Ansible 本身的模组,必须是 Python 或 Powershell 的。
将 Python 或 Powershell 用于咱们定制模组的一个好处是,可以使用 module_utils
通用代码,完成参数处理、日志记录和响应编写等大量繁重工作。
创建一个模组
强烈建议咱们在 Python 开发中,使用 venv
或 virtualenv
。
要创建一个模组:
- 在咱们的工作区中创建一个
library
目录。咱们的测试 playbook 也应存在于同一目录下; - 创建咱们的新模组文件
$ touch library/my_test.py
。或者以咱们选择的编辑器打开/创建他; - 将下面的内容粘贴到咱们的新模组文件中。其中包括了 所需的 Ansible 格式与文档,简单用于声明模组选项的参数规范,以及一些示例代码;
- 修改并扩展代码,以实现咱们打算咱们的新模组要做的事情。关于如何编写简洁的模组代码,请参阅 编程技巧 和 Python 3 兼容性 页面。
#!/usr/bin/python
# Copyright: (c) 2018, Terry Jones <terry.jones@example.org>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: my_test
short_description: This is my test module
# If this is part of a collection, you need to use semantic versioning,
# i.e. the version is of the form "2.5.0" and not "2.4".
version_added: "1.0.0"
description: This is my longer description explaining my test module.
options:
name:
description: This is the message to send to the test module.
required: true
type: str
new:
description:
- Control to demo if the result of this module is changed or not.
- Parameter description can be a list as well.
required: false
type: bool
# Specify this value according to your collection
# in format of namespace.collection.doc_fragment_name
# extends_documentation_fragment:
# - my_namespace.my_collection.my_doc_fragment_name
author:
- Your Name (@yourGitHubHandle)
'''
EXAMPLES = r'''
# Pass in a message
- name: Test with a message
my_namespace.my_collection.my_test:
name: hello world
# pass in a message and have changed true
- name: Test with a message and changed output
my_namespace.my_collection.my_test:
name: hello world
new: true
# fail the module
- name: Test failure of the module
my_namespace.my_collection.my_test:
name: fail me
'''
RETURN = r'''
# These are examples of possible return values, and in general should use other names for return values.
original_message:
description: The original name param that was passed in.
type: str
returned: always
sample: 'hello world'
message:
description: The output message that the test module generates.
type: str
returned: always
sample: 'goodbye'
'''
from ansible.module_utils.basic import AnsibleModule
def run_module():
# define available arguments/parameters a user can pass to the module
module_args = dict(
name=dict(type='str', required=True),
new=dict(type='bool', required=False, default=False)
)
# seed the result dict in the object
# we primarily care about changed and state
# changed is if this module effectively modified the target
# state will include any data that you want your module to pass back
# for consumption, for example, in a subsequent task
result = dict(
changed=False,
original_message='',
message=''
)
# the AnsibleModule object will be our abstraction working with Ansible
# this includes instantiation, a couple of common attr would be the
# args/params passed to the execution, as well as if the module
# supports check mode
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)
# if the user is working with this module in only check mode we do not
# want to make any changes to the environment, just return the current
# state with no modifications
if module.check_mode:
module.exit_json(**result)
# manipulate or modify the state as needed (this is going to be the
# part where your module will do what it needs to do)
result['original_message'] = module.params['name']
result['message'] = 'goodbye'
# use whatever logic you need to determine whether or not this module
# made any modifications to your target
if module.params['new']:
result['changed'] = True
# during the execution of the module, if there is an exception or a
# conditional state that effectively causes a failure, run
# AnsibleModule.fail_json() to pass in the message and the result
if module.params['name'] == 'fail me':
module.fail_json(msg='You requested this to fail', **result)
# in the event of a successful module execution, you will want to
# simple AnsibleModule.exit_json(), passing the key/value results
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()
创建信息或事实模组
Ansible 使用 facts
模组收集目标机器的信息,使用 info
模组收集其他对象或文件的信息。如果咱们发现自己试图往现有模组中添加 state: info
或 state: list
,便是需要一个新的专门 _facts
或 _info
模组的迹象。
在 Ansible 2.8 及以后版本中,我们有两种类型的信息模组,分别是 *_info
和 *_facts
。
如果某个模组被命名为 <something>_facts
,那是因为他的主要目的是返回 ansible_facts
。不要用 _facts
来命名那些不用于此目的的模组。只将 ansible_facts
用于获取主机的特定信息,例如网络接口及其配置、安装的操作系统和程序等。
查询/返回一般信息(而非 ansible_facts
)的模组应命名为 _info
。一般信息是那些非主机特定的信息,例如在线/云服务的信息(咱们可以访问到同一台主机中同一在线服务的不同账户),或该机器上可访问的虚拟机与容器的信息,或单个文件或程序的信息等。
这些 info
和 facts
模组与其他 Ansible 模组别无二致,只是有些小要求:
- 他们必须以
<something>_info
或<something>_facts
命名,其中<something>
是单数; info
类的*_info
模组,必须 以字典结果的形式返回,以便别的模组可以访问他们;facts
类的*_facts
模组,必须 返回结果字典中的ansible_facts
字段,以便其他模组可以访问他们;- 他们 必须 支持
check_mode
; - 他们 必须不会 对系统造成改变;
- 他们 必须 为 返回值字段 编写文档,并编写 示例。
咱们可将咱们的事实,像下面这样添加到结果的 ansible_facts
字段:
module.exit_json(changed=False, ansible_facts=dict(my_new_fact=value_of_fact))
其余就跟创建普通模组一样。
验证咱们的模组代码
在咱们修改了上面的示例代码,实现咱们想要的功能后,咱们就可以试用咱们的模组了。如果咱们在验证模组代码时遇到错误,我们的 调试技巧 将有所帮助。
在本地验证咱们的模组代码
最简单的方法,是使用 ansible
这个 adhoc 命令:
ANSIBLE_LIBRARY=./library ansible -m my_test -a 'name=hello new=true' remotehost
若咱们的模组不需要以某个远端主机为目标,咱们可像下面这样快速、轻松地在本地运行咱们的代码:
ANSIBLE_LIBRARY=./library ansible -m my_test -a 'name=hello new=true' localhost
- 如果出于任何原因(
pdb
、使用print()
、更快的迭代等),咱们想要避免通过 Ansible,那么另一种方法就是创建一个参数文件,即一个向其传递参数以便运行该模组基本 JSON 配置文件。将该参数文件命名为/tmp/args.json
,并添加以下内容:
{
"ANSIBLE_MODULE_ARGS": {
"name": "hello",
"new": true
}
}
- 然后该模组便可在本地直接测试。这样做省略了打包步骤,且直接使用了
module_utils
文件:
$ python library/my_test.py /tmp/args.json
这应返回如下的输出:
{"changed": true, "state": {"original_message": "hello", "new_message": "goodbye"}, "invocation": {"module_args": {"name": "hello", "new": true}}}
在某个 playbook 中验证咱们的模组代码
通过将其包含在某个 playbook 中,咱们可以轻松运行一个完整测试,只需 library
目录与 play 在同一个目录下:
- 在
library
所在目录下创建一个 playbook:touch test_mod.yml
; - 将以下内容添加到这个新的 playbook 文件:
- name: test my new module
hosts: localhost
tasks:
- name: run the new module
my_test:
name: 'hello'
new: true
register: testout
- name: dump test output
debug:
msg: '{{ testout }}'
- 运行该 playbook 并分析输出:
$ ansible-playbook test_mod.yml
测试咱们新创建的模组
查看我们的 测试 小节,了解更多详细信息,包括有关 测试模组文档、添加 集成测试 等的说明。
注意:若要贡献到 Ansible,那么每个新模组和插件,都应有集成测试,即使这些测试无法在 Ansible CI 基础设施上运行。在这种情况下,应在 别名文件 中用
unsupported
别名标记这些测试。
回馈 Ansible
如果咱们想通过添加新功能或修复 bug 为 ansible-core
作出贡献,请创建一个 ansible/ansible
代码仓库的分叉,并以 devel
分支为起点,开发某个新的特性分支。当咱们有了良好的工作代码变更时,咱们可以选择咱们的特性分支作为源,Ansible devel
分支作为目标,向 Ansible 代码库提交拉取请求。
若咱们打算为某个 Ansible 专辑 贡献模块,请查看我们的 提交检查单、编程技巧、维护 Python 2 和 Python 3 兼容性的策略,以及在打开拉取请求前进行 测试 的相关信息。
社区指南 涵盖了如何开启拉取请求,以及接下来会发生的事情。
交流与开发支持
请访问 Ansible 交流指南,了解如何加入对话。
致谢
感谢 Thomas Stringer (@trstringer
) 为这个主题贡献原始资料。
(End)