约定、技巧和陷阱
在设计和开发模块时,请遵循以下基本约定和技巧,以获得干净、可用的代码
模块的范围
特别是如果您想将您的模块贡献给现有的 Ansible 集合,请确保每个模块都包含足够的逻辑和功能,但不要太多。 如果这些指南看起来令人困惑,请考虑 是否真的需要编写模块。
每个模块都应该具有简洁且定义明确的功能。 基本上,遵循 UNIX 的“做好一件事”的哲学。
不要向现有模块添加
get
、list
或info
状态选项 - 创建新的_info
或_facts
模块。模块不应该要求用户了解要使用的 API/工具的所有底层选项。 例如,如果必需的模块选项的合法值无法记录,则该模块不属于 Ansible Core。
模块应该包含与资源交互的大部分逻辑。 一个围绕复杂 API 的轻量级包装器会迫使用户将太多逻辑卸载到他们的剧本中。 如果您想将 Ansible 连接到一个复杂的 API,创建多个模块 来与 API 的较小单个部分进行交互。
避免创建一个执行其他模块工作的模块; 这会导致代码重复和差异,并使事物变得不统一、不可预测和更难维护。 模块应该是构建块。 如果您问“如何让一个模块执行其他模块”……您想要编写一个角色。
设计模块接口
如果您的模块正在处理一个对象,那么该对象的选项应该尽可能地称为
name
,或者接受name
作为别名。接受布尔状态的模块应该接受
yes
、no
、true
、false
或用户可能输入的任何其他内容。 AnsibleModule 公共代码使用type='bool'
支持这一点。避免
action
/command
,它们是命令式的,而不是声明式的,还有其他方法可以表达相同的意思。
一般指南和技巧
每个模块都应该在一个文件中自包含,这样它就可以被
ansible-core
自动传输。模块名称 MUST 使用下划线而不是连字符或空格作为单词分隔符。 使用连字符和空格会阻止
ansible-core
导入您的模块。在开发模块时始终使用
hacking/test-module.py
脚本 - 它会警告您常见的陷阱。如果您有一个返回特定于您的安装的信息的本地模块,那么该模块的一个好名字是
site_info
。消除或最小化依赖项。 如果您的模块具有依赖项,请在模块文件顶部记录它们,并在依赖项导入失败时引发 JSON 错误消息。
不要直接写入文件; 使用一个临时文件,然后使用
atomic_move
函数(来自ansible.module_utils.basic
)将更新后的临时文件移动到位。 这可以防止数据损坏,并确保文件保留正确的上下文。避免创建缓存。 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()
处理模块故障
当您的模块出现故障时,请帮助用户了解问题所在。如果您使用的是 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/stopped
、present/absent
。努力实现一致的最终状态(即幂等性)。如果对同一系统连续两次运行模块会导致两种不同的状态,请查看是否可以重新设计或重写以实现一致的最终状态。如果做不到,请记录该行为及其原因。
在标准 Ansible 返回结构中提供一致的返回值,即使 NA/None 用于在其他选项下通常返回的键。
模块安全
避免从 shell 传递用户输入。
始终检查返回码。
您必须始终使用
module.run_command
,而不是subprocess
或Popen
或os.system
。避免使用 shell,除非绝对必要。
如果您必须使用 shell,您必须将
use_unsafe_shell=True
传递给module.run_command
。如果您的模块中的任何变量都可能来自
use_unsafe_shell=True
的用户输入,则您必须使用pipes.quote(x)
将其包装。在获取 URL 时,请使用
fetch_url
或open_url
来自ansible.module_utils.urls
。不要使用urllib2
,它不会原生验证 TLS 证书,因此对于 https 不安全。标记为
no_log=True
的敏感值将自动从模块返回值中删除该值。如果您的模块可能将这些敏感值作为字典键名的一部分返回,则应调用ansible.module_utils.basic.sanitize_keys()
函数以从键中删除这些值。请参阅uri
模块以了解示例。