约定、技巧和陷阱

在设计和开发模块时,请遵循以下基本约定和技巧,以获得干净、可用的代码

模块的范围

特别是如果你想将你的模块贡献到现有的 Ansible 集合,请确保每个模块包含足够的逻辑和功能,但不要太多。如果这些指南看起来令人困惑,请考虑 是否真的需要编写模块

  • 每个模块应该具有简洁且定义明确的功能。基本上,遵循 UNIX 的哲学:做好一件事。

  • 不要向现有模块添加 getlistinfo 状态选项 - 创建一个新的 _info_facts 模块。

  • 模块不应该要求用户了解要使用的 API/工具的所有底层选项。例如,如果无法记录必需模块选项的合法值,则该模块不属于 Ansible Core。

  • 模块应该包含与资源交互的大部分逻辑。围绕复杂 API 的轻量级包装器会迫使用户将太多逻辑卸载到他们的剧本中。如果你想将 Ansible 连接到复杂 API,创建多个模块,这些模块与 API 的较小的单个部分进行交互。

  • 避免创建执行其他模块工作的模块;这会导致代码重复和分歧,并使事情变得不统一、不可预测和难以维护。模块应该是构建块。如果你问“如何让一个模块执行其他模块”……你需要编写一个角色。

设计模块接口

  • 如果你的模块正在处理一个对象,则该对象的选项应尽可能被称为 name,或接受 name 作为别名。

  • 接受布尔状态的模块应该接受 yesnotruefalse 或用户可能遇到的任何其他东西。AnsibleModule 公共代码使用 type='bool' 支持这一点。

  • 避免 action/command,它们是命令式的,而不是声明式的,还有其他方法可以表达相同的意思。

一般指南和技巧

  • 每个模块都应该在一个文件中自包含,以便 ansible-core 可以自动传输它。

  • 模块名称必须使用下划线而不是连字符或空格作为单词分隔符。使用连字符和空格将阻止 ansible-core 导入你的模块。

  • 在开发模块时,始终使用 hacking/test-module.py 脚本 - 它会警告你一些常见的陷阱。

  • 如果你有一个返回有关安装的特定信息的本地模块,一个好的名称是 site_info

  • 消除或最小化依赖关系。如果你的模块有依赖关系,请在模块文件顶部记录它们,并在导入失败时引发 JSON 错误消息。

  • 不要直接写入文件;使用临时文件,然后使用 atomic_move 函数(来自 ansible.module_utils.basic)将更新的临时文件移到适当的位置。这可以防止数据损坏,并确保文件的正确上下文得到保留。

  • 避免创建缓存。Ansible 的设计没有中央服务器或权限,因此你无法保证它不会以不同的权限、选项或位置运行。如果你需要一个中央权限,请将其放在 Ansible 之上(例如,使用堡垒/CM/CI 服务器、AWX 或 Red Hat Ansible 自动化平台);不要尝试将其构建到模块中。

  • 如果你将模块打包到 RPM 中,请将模块安装在控制机器上的 /usr/share/ansible 中。将模块打包到 RPM 中是可选的。

函数和方法

  • 每个函数都应该简洁,并应该描述有意义的工作量。

  • “不要重复自己”通常是一个很好的哲学。

  • 函数名应该使用下划线:my_function_name

  • 每个函数的名称都应该描述该函数的功能。

  • 每个函数都应该有一个文档字符串。

  • 如果你的代码嵌套太多,这通常表明循环主体可以从成为函数中获益。我们现有的代码部分有时不是这方面的最佳示例。

Python 技巧

  • 包含一个 main 函数,它封装正常执行。

  • 从条件调用你的 main 函数,这样你就可以将其导入到单元测试中 - 例如

if __name__ == '__main__':
    main()

导入和使用共享代码

  • 尽可能使用共享代码 - 不要重新造轮子。Ansible 提供了 AnsibleModule 公共 Python 代码,以及 实用程序,用于许多常见用例和模式。你还可以创建适用于多个模块的文档片段。

  • 在与导入其他库相同的位置导入 ansible.module_utils 代码。

  • 不要使用通配符 (*) 导入其他 python 模块;相反,列出你正在导入的函数(例如,from some.other_python_module.basic import otherFunction)。

  • try/except 中导入自定义包,捕获任何导入错误,并在 main() 中使用 fail_json() 处理它们。例如

import traceback

from ansible.module_utils.basic import missing_required_lib

LIB_IMP_ERR = None
try:
    import foo
    HAS_LIB = True
except:
    HAS_LIB = False
    LIB_IMP_ERR = traceback.format_exc()

然后在 main() 中,在 argspec 之后,执行以下操作

if not HAS_LIB:
    module.fail_json(msg=missing_required_lib("foo"),
                     exception=LIB_IMP_ERR)

并在模块的 DOCUMENTATION 块requirements 部分中记录依赖关系。

处理模块故障

当你的模块出现故障时,请帮助用户了解发生了什么错误。如果你正在使用 AnsibleModule 公共 Python 代码,当你调用 fail_json 时,failed 元素将自动包含在内。为了实现礼貌的模块故障行为

  • msg 中包含 failed 键以及字符串解释。如果你没有这样做,Ansible 将使用标准返回代码:0=成功,非零=失败。

  • 不要引发回溯(堆栈跟踪)。Ansible 可以处理堆栈跟踪,并自动将任何无法解析的内容转换为失败结果,但在模块失败时引发堆栈跟踪并不友好。

  • 不要使用 sys.exit()。使用模块对象中的 fail_json()

优雅地处理异常(错误)

  • 提前验证 - 快速失败并返回有用且清晰的错误消息。

  • 使用防御性编程 - 为你的模块使用简单的设计,优雅地处理错误,并避免直接堆栈跟踪。

  • 可预测地失败 - 如果我们必须失败,请以最预期的方式失败。模拟底层工具或系统工作的一般方式。

  • 提供有关你正在做什么的有用消息,并将异常消息添加到其中。

  • 避免使用catchall异常,除非底层API提供了与尝试的操作相关的非常好的错误消息,否则它们没有太大用处。

创建正确且信息丰富的模块输出

模块必须仅输出有效的JSON。请遵循以下指南以创建正确、有用的模块输出

  • 模块返回数据必须以严格的UTF-8编码。无法返回UTF-8编码数据的模块应返回使用base64等编码的数据。可选地,模块可以确定它们是否可以编码为UTF-8并使用 errors='replace' 来替换非UTF-8字符,从而使返回值有损。

  • 将您的顶级返回类型设为哈希(字典)。

  • 将复杂返回值嵌套在顶级哈希中。

  • 将任何列表或简单标量值包含在顶级返回哈希中。

  • 不要将模块输出发送到标准错误,因为系统会将标准输出与标准错误合并,并阻止解析JSON。

  • 捕获标准错误并将其作为标准输出上的JSON中的变量返回。这就是command模块的实现方式。

  • 永远不要在模块中执行 print("some status message"),因为它不会生成有效的JSON输出。

  • 始终返回有用的数据,即使没有更改也是如此。

  • 关于返回保持一致(有些模块太随机),除非这对状态/操作不利。

  • 使返回值可重用 - 大多数情况下您不需要读取它,但您需要处理它并重新利用它。

  • 如果处于diff模式,则返回diff。这不是所有模块都要求的,因为对于某些模块来说没有意义,但请在适用时包含它。

  • 使用Python的标准 JSON编码器和解码器 库将您的返回值序列化为JSON。基本Python类型(字符串、int、字典、列表等)是可序列化的。

  • 不要使用exit_json() 返回对象。相反,将您需要的字段从对象转换为字典的字段并返回字典。

  • 来自多个主机的结果将同时聚合,因此您的模块应该只返回相关的输出。返回日志文件的全部内容通常是不好的做法。

如果模块返回stderr或无法生成有效的JSON,实际输出仍将在Ansible中显示,但命令将不会成功。

遵循Ansible惯例

Ansible惯例在所有模块、剧本和角色中提供可预测的用户界面。要遵循您模块开发中的Ansible惯例

  • 在模块之间使用一致的名称(是的,我们有很多遗留偏差 - 不要使问题更严重!)。

  • 在您的模块中使用一致的选项(参数)。

  • 不要使用“message”或“syslog_facility”作为选项名称,因为Ansible在内部使用它们。

  • 将选项与其他模块规范化 - 如果Ansible和您的模块连接到的API对相同选项使用不同的名称,请为您的选项添加别名,以便用户可以选择在任务和剧本中使用哪个名称。

  • 将来自 *_facts 模块的事实返回到 ansible_facts 字段中 result字典 中,以便其他模块可以访问它们。

  • 在所有 *_info*_facts 模块中实现 check_mode。根据事实信息进行条件化的剧本,只有在 check_mode 中返回了事实,才能在 check_mode 中正确地进行条件化。通常,您可以在实例化 AnsibleModule 时添加 supports_check_mode=True

  • 使用特定于模块的环境变量。例如,如果您在 module_utils.api 中使用帮助程序进行基本身份验证,并使用 module_utils.urls.fetch_url(),并且您回退到环境变量以获取默认值,请使用特定于模块的环境变量,例如 API_<MODULENAME>_USERNAME 来避免模块之间的冲突。

  • 保持模块选项简单且集中 - 如果您在现有选项上加载了许多选择/状态,请考虑添加一个新的、简单的选项。

  • 尽可能保持选项小巧。将大型数据结构传递给选项可能会为我们节省一些任务,但它添加了一个复杂的要求,我们无法在传递给模块之前轻松验证。

  • 如果您想将复杂数据传递给选项,请编写一个允许此操作的专家模块,以及几个提供对底层 API 和服务的更“原子”操作的较小模块。复杂的操作需要复杂的数据。让用户选择是在任务和剧本中反映这种复杂性,还是在变量文件中反映。

  • 实现声明性操作(而不是 CRUD),以便用户可以忽略现有状态并专注于最终状态。例如,使用 started/stoppedpresent/absent

  • 努力实现一致的最终状态(也称为幂等性)。如果在同一系统上连续运行两次模块会导致两种不同的状态,请查看是否可以重新设计或重写以实现一致的最终状态。如果您不能,请记录行为及其原因。

  • 在标准Ansible返回结构中提供一致的返回值,即使NA/None用于通常在其他选项下返回的键也是如此。

模块安全性

  • 避免从shell传递用户输入。

  • 始终检查返回码。

  • 您必须始终使用 module.run_command,而不是 subprocessPopenos.system

  • 避免使用shell,除非绝对必要。

  • 如果您必须使用shell,则必须将 use_unsafe_shell=True 传递给 module.run_command

  • 如果模块中的任何变量都可能来自使用 use_unsafe_shell=True 的用户输入,则必须将它们包装在 pipes.quote(x) 中。

  • 在获取URL时,请使用 fetch_urlopen_url 来自 ansible.module_utils.urls。不要使用 urllib2,它不会原生验证TLS证书,因此对于https是不安全的。

  • 标记为 no_log=True 的敏感值将自动从模块返回值中删除。如果您的模块可能将这些敏感值作为字典键名的一部分返回,您应该调用 ansible.module_utils.basic.sanitize_keys() 函数从键中删除这些值。请参阅 uri 模块以获取示例。