开发模块

模块是可重用的独立脚本,由 Ansible 代表您在本地或远程运行。模块与您的本地机器、API 或远程系统交互,以执行特定任务,例如更改数据库密码或启动云实例。每个模块都可以由 Ansible API 使用,也可以由 ansibleansible-playbook 程序调用。模块提供定义的接口,接受参数,并在退出前通过向 stdout 打印 JSON 字符串将信息返回给 Ansible。

如果您需要的功能在集合(collections)中现有的数千个 Ansible 模块中无法找到,您可以轻松编写自己的自定义模块。当您编写用于本地使用的模块时,您可以选择任何编程语言并遵循您自己的规则。请使用本主题了解如何使用 Python 创建 Ansible 模块。创建模块后,必须将其添加到本地相应的目录中,以便 Ansible 能够找到并执行它。有关在本地添加模块的详细信息,请参阅 在本地添加模块和插件

如果您正在集合 (collection)中开发模块,请参阅相关文档。

为开发 Ansible 模块准备环境

您只需要安装 ansible-core 即可测试模块。模块可以用任何语言编写,但以下指南大部分假设您使用的是 Python。要包含在 Ansible 自身中的模块必须使用 Python 或 PowerShell 编写。

为自定义模块使用 Python 或 PowerShell 的一个优势是可以利用 module_utils 公共代码,它承担了参数处理、日志记录和响应编写等大量繁重工作。

创建独立模块

强烈建议您在 Python 开发中使用 venvvirtualenv

创建独立模块的步骤:

  1. 在您的工作空间中创建一个 library 目录。您的测试 Playbook 应放在同一个目录下。

  2. 创建新的模块文件:$ touch library/my_test.py。或者使用您喜欢的编辑器打开/创建它。

  3. 将以下内容粘贴到您的新模块文件中。它包含了必需的 Ansible 格式和文档、用于声明模块选项的简单参数规范,以及一些示例代码。

  4. 修改并扩展代码以实现您想要的新模块功能。有关编写简洁明了模块代码的提示,请参阅编程技巧Python 3 兼容性页面。

在集合中创建模块

要在名为 my_namespace.my_collection 的现有集合中创建新模块:

  1. 创建新的模块文件:$ touch <PATH_TO_COLLECTION>/ansible_collections/my_namespace/my_collection/plugins/modules/my_test.py。或者直接使用您喜欢的编辑器创建它。

  2. 将以下内容粘贴到您的新模块文件中。它包含了必需的 Ansible 格式和文档、用于声明模块选项的简单参数规范,以及一些示例代码。

  3. 修改并扩展代码以实现您想要的新模块功能。有关编写简洁明了模块代码的提示,请参阅编程技巧Python 3 兼容性页面。

#!/usr/bin/python

# Copyright: (c) 2018, Terry Jones <terry.jones@example.org>
# GNU General Public License v3.0+ (see COPYING or https://gnu.net.cn/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: infostate: list,这通常表明需要一个新的专用 _facts_info 模块。

在 Ansible 2.8 及以后的版本中,我们有两种类型的信息模块,分别是 *_info*_facts

如果模块被命名为 <something>_facts,其主要目的应该是返回 ansible_facts。不要将不执行此操作的模块命名为 _facts。仅对特定于主机的信息使用 ansible_facts,例如网络接口及其配置、操作系统以及安装了哪些程序。

查询/返回通用信息(而非 ansible_facts)的模块应命名为 _info。通用信息是指非主机特定的信息,例如关于在线/云服务的信息(您可以从同一主机访问同一在线服务的不同账户)、从机器可访问的虚拟机和容器的信息,或关于单个文件或程序的信息。

信息模块和事实模块与其他任何 Ansible 模块一样,仅有几点细微要求:

  1. 它们必须命名为 <something>_info<something>_facts,其中 <something> 为单数。

  2. 信息模块 *_info 必须以结果字典 (result dictionary)的形式返回,以便其他模块可以访问它们。

  3. 事实模块 *_facts 必须在结果字典ansible_facts 字段中返回,以便其他模块可以访问它们。

  4. 它们必须支持 检查模式 (check_mode)

  5. 它们不得对系统进行任何更改。

  6. 它们必须记录返回字段示例

您可以按照以下方式将事实添加到结果的 ansible_facts 字段中:

module.exit_json(changed=False, ansible_facts=dict(my_new_fact=value_of_fact))

其余部分与创建普通模块相同。

验证您的模块代码

在修改上述示例代码以实现您的需求后,您可以尝试运行该模块。如果您在验证模块代码时遇到错误,我们的调试技巧将对您有所帮助。

在本地验证您的模块代码

最简单的方法是使用 ansible 即席命令 (adhoc command):

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

对于在名为 my_namespace.my_collection 的现有集合中开发的模块(如上所述):

$ ansible localhost -m my_namespace.my_collection.my_test -a 'name=hello new=true' --playbook-dir=$PWD
  • 如果您使用 pdbprint() 或其他本地调试方法以加快迭代速度,可以通过创建一个参数文件来避免通过 Ansible 运行。这是一个基本的 JSON 配置文件,用于向您的模块传递参数,以便您可以直接运行它。将参数文件命名为 /tmp/args.json 并添加以下内容:

{
    "ANSIBLE_MODULE_ARGS": {
        "name": "hello",
        "new": true
    }
}
  • 然后就可以在本地直接测试该模块。这将跳过打包步骤并直接使用 module_utils 文件:

$ python library/my_test.py /tmp/args.json

您可能还需要将集合的路径添加到 Python 路径中。这会告诉 Python 在给定路径中查找额外的 module_utils 代码。您可以像以下示例一样运行模块代码:

$ export PYTHONPATH=PATH_TO_COLLECTIONS:$PYTHONPATH
$ python -m ansible_collections.my_namespace.my_collection.plugins.modules.my_test /tmp/args.json

它应该返回类似这样的输出:

{"changed": true, "state": {"original_message": "hello", "new_message": "goodbye"}, "invocation": {"module_args": {"name": "hello", "new": true}}}

在 Playbook 中验证您的模块代码

只要 library 目录与 Playbook 位于同一目录,您就可以通过将其包含在 Playbook 中来轻松运行完整测试:

  • 在任何目录中创建 Playbook:$ touch testmod.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 ./testmod.yml

测试您新创建的模块

请查阅我们的测试部分以获取更详细的信息,包括有关测试模块文档、添加集成测试等说明。

注意

如果为 Ansible 做出贡献,每个新模块和插件都应包含集成测试,即使这些测试无法在 Ansible CI 基础设施上运行。在这种情况下,测试应在别名文件 (aliases file)中标记为 unsupported 别名。

向 Ansible 回馈贡献

如果您想通过添加新功能或修复错误为 ansible-core 做出贡献,请创建 fork 仓库,并使用 devel 分支作为起点开发新特性分支。当您拥有良好的可运行代码更改后,可以通过选择您的特性分支作为源、Ansible devel 分支作为目标,向 Ansible 仓库提交 pull request。

如果您想向Ansible 集合贡献模块,请在打开 pull request 之前查看我们的提交清单编程技巧维护 Python 2 和 Python 3 兼容性的策略以及有关测试的信息。

社区指南涵盖了如何打开 pull request 以及后续流程。

沟通与开发支持

访问Ansible 沟通指南以获取有关如何参与交流的信息。

致谢

感谢 Thomas Stringer (@trstringer) 为本主题贡献素材。