约定、技巧和陷阱
在设计和开发模块时,请遵循这些基本约定和技巧,以获得干净、可用的代码
确定模块的作用域
特别是如果你想将你的模块贡献给现有的 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
自动传输。模块名称必须使用下划线而不是连字符或空格作为单词分隔符。使用连字符和空格将阻止
ansible-core
导入你的模块。在开发模块时,始终使用
hacking/test-module.py
脚本 - 它会警告你常见的陷阱。如果你有一个返回特定于你的安装的信息的本地模块,则该模块的一个好名称是
site_info
。消除或最小化依赖项。如果你的模块有依赖项,请在模块文件的顶部记录它们,并在依赖项导入失败时引发 JSON 错误消息。
不要直接写入文件;使用临时文件,然后使用
ansible.module_utils.basic
中的atomic_move
函数将更新的临时文件移动到位。这可以防止数据损坏,并确保保留文件的正确上下文。避免创建缓存。Ansible 的设计没有中央服务器或授权机构,因此你无法保证它不会使用不同的权限、选项或位置运行。如果你需要中央授权机构,请将其放在 Ansible 之上(例如,使用堡垒/cm/ci 服务器、AWX 或红帽 Ansible 自动化平台);不要尝试将其构建到模块中。
如果将你的模块打包到 RPM 中,请将模块安装在控制机器上的
/usr/share/ansible
中。将模块打包到 RPM 中是可选的。
函数和方法
每个函数都应该简洁,并且应该描述有意义的工作量。
“不要重复自己”通常是一个好理念。
函数名称应使用下划线:
my_function_name
。每个函数的名称都应描述函数的作用。
每个函数都应该有一个文档字符串。
如果你的代码嵌套太深,这通常表明循环体可以从成为函数中受益。我们现有代码的某些部分有时不是这方面的最佳示例。
Python 技巧
包括一个包装正常执行的
main
函数。从条件调用你的
main
函数,以便你可以将其导入到单元测试中 - 例如
if __name__ == '__main__':
main()
处理模块故障
当你的模块失败时,帮助用户了解哪里出了问题。如果你正在使用 AnsibleModule
通用 Python 代码,则当你调用 fail_json
时,将为你自动包含 failed
元素。为了礼貌的模块失败行为
在
msg
中包含一个failed
键和一个字符串说明。如果你不这样做,Ansible 将使用标准返回代码:0=成功,非零=失败。不要引发回溯(堆栈跟踪)。Ansible 可以处理堆栈跟踪,并自动将任何无法解析的内容转换为失败的结果,但是在模块失败时引发堆栈跟踪并不用户友好。
不要使用
sys.exit()
。使用模块对象中的fail_json()
。
优雅地处理异常(错误)
预先验证 - 快速失败并返回有用且清晰的错误消息。
使用防御性编程——为你的模块使用简单的设计,优雅地处理错误,并避免直接显示堆栈跟踪信息。
可预测地失败——如果必须失败,也要以最期望的方式进行。要么模仿底层工具的行为,要么模仿系统的一般工作方式。
给出关于你正在做什么的有用消息,并添加异常信息。
避免使用捕获所有异常的语句,除非底层 API 提供了关于尝试操作的非常好的错误消息,否则它们不是很有用。
创建正确且信息丰富的模块输出
模块必须仅输出有效的 JSON。请遵循以下指导原则来创建正确、有用的模块输出
模块返回的数据必须使用严格的 UTF-8 编码。无法返回 UTF-8 编码数据的模块应使用诸如 base64 之类的编码方式对数据进行编码。或者,模块可以自行决定是否可以使用 UTF-8 编码,并利用
errors='replace'
来替换非 UTF-8 字符,从而使返回值有损。将你的顶层返回类型设置为哈希(字典)。
将复杂的返回值嵌套在顶层哈希中。
将任何列表或简单的标量值包含在顶层返回哈希中。
不要将模块输出发送到标准错误,因为系统会将标准输出与标准错误合并,从而阻止 JSON 解析。
捕获标准错误,并将其作为 JSON 中的一个变量在标准输出中返回。这是命令模块的实现方式。
永远不要在模块中使用
print("一些 状态 消息")
,因为它不会产生有效的 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
中使用 helper 来通过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 时,请使用
ansible.module_utils.urls
中的fetch_url
或open_url
。不要使用urllib2
,因为它不能原生验证 TLS 证书,因此对于 https 是不安全的。标记为
no_log=True
的敏感值将自动从模块返回值中删除该值。如果你的模块可以将这些敏感值作为字典键名称的一部分返回,则应调用ansible.module_utils.basic.sanitize_keys()
函数从键中删除这些值。有关示例,请参见uri
模块。