约定、技巧和陷阱

在设计和开发模块时,请遵循以下基本约定和技巧,以编写简洁易用的代码

模块作用域

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

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

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

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

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

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

模块接口设计

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

  • 接受布尔状态的模块应该接受yesnotruefalse,或用户可能使用的任何其他内容。AnsibleModule 通用代码使用type='bool'支持这一点。

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

一般指南和技巧

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

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

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

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

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

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

  • 避免创建缓存。Ansible 的设计没有中央服务器或权威机构,因此您无法保证它不会以不同的权限、选项或位置运行。如果您需要中央权威机构,请将其置于 Ansible 之上(例如,使用堡垒/cm/ci 服务器、AWX 或 Red Hat Ansible Automation Platform);不要尝试将其构建到模块中。

  • 如果您将模块打包到 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元素会自动包含在内。对于友好的模块故障行为

  • 包含一个键failed以及msg中的字符串说明。如果您不这样做,Ansible 将使用标准返回码:0=成功,非零=失败。

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

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

优雅地处理异常(错误)

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

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

  • 可预测地失败——如果必须失败,请以最预期的方式进行。模仿底层工具或系统的一般工作方式。

  • 提供关于您正在执行的操作的有用消息,并将异常消息添加到其中。

  • 避免使用通配符异常,除非底层 API 提供了关于尝试操作的非常好的错误消息,否则它们不太有用。

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

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

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

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

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

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

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

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

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

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

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

  • 使返回值可重用——大多数时候您不想读取它,但您确实希望处理它并将其重新利用。

  • 如果处于 diff 模式,则返回 diff。并非所有模块都需要这样做,因为某些模块没有意义,但请在适用时包含它。

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

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

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

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

遵循 Ansible 约定

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

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

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

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

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

  • *_facts模块返回的事实位于结果字典ansible_facts字段中,以便其他模块可以访问它们。

  • 在所有*_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 和服务的更“原子”的操作。复杂的操作需要复杂的数据。让用户选择是在任务和剧本中反映这种复杂性,还是在 vars 文件中反映这种复杂性。

  • 实现声明式操作(而不是 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 时,请使用ansible.module_utils.urls中的fetch_urlopen_url。不要使用urllib2,因为它不会原生验证 TLS 证书,因此对于 https 不安全。

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