约定、技巧和常见陷阱
在设计和开发模块时,请遵循这些基本约定和技巧,以获得清晰、可用的代码
模块的范围界定
特别是如果您想将模块贡献给现有的 Ansible Collection,请确保每个模块包含足够的逻辑和功能,但不要过多。如果这些准则看起来令人困惑,请考虑您是否真的需要编写模块。
每个模块都应具有简洁明确的功能。基本上,遵循 UNIX 哲学,即做好一件事。
不要向现有模块添加
get、list或info状态选项 — 请创建新的_info或_facts模块。模块不应要求用户了解所有底层 API/工具选项才能使用。例如,如果无法文档化所需模块选项的合法值,则该模块不属于 Ansible 核心。
模块应包含与资源交互的大部分逻辑。围绕复杂 API 的轻量级包装器会强制用户将过多逻辑卸载到他们的 playbook 中。如果您想将 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 或 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=成功,非零=失败。不要引发回溯(stacktrace)。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 输出。即使没有更改,也始终返回有用的数据。
保持返回一致(有些模块过于随机),除非这会损害状态/操作。
使返回可重用——大多数时候您不需要阅读它,但您确实希望处理和重新利用它。
如果处于差异模式,则返回差异。这并非所有模块都必需,因为对某些模块来说没有意义,但请在适用时包含它。
使用 Python 的标准 JSON 编码器和解码器库,使您的返回值可序列化为 JSON。基本的 Python 类型(字符串、整数、字典、列表等)都是可序列化的。
不要使用 exit_json() 返回对象。而是将您从对象中需要的字段转换为字典的字段并返回字典。
来自许多主机的结果将一次性聚合,因此您的模块应仅返回相关输出。返回日志文件的全部内容通常是一种糟糕的形式。
如果模块返回 stderr 或未能生成有效的 JSON,实际输出仍将显示在 Ansible 中,但命令不会成功。
遵循 Ansible 约定
Ansible 约定在所有模块、playbook 和角色中提供可预测的用户界面。要在模块开发中遵循 Ansible 约定
在模块之间使用一致的名称(是的,我们有许多历史遗留的偏差——不要让问题变得更糟!)。
在您的模块中(或多个模块之间)使用一致的选项(参数)。
不要使用“message”或“syslog_facility”作为选项名称,因为这些是 Ansible 内部使用的。
与其他模块规范化选项——如果 Ansible 和您的模块连接的 API 对同一选项使用不同的名称,请为您的选项添加别名,以便用户可以选择在任务和 playbook 中使用哪个名称。
将
*_facts模块的事实返回到结果字典的ansible_facts字段中,以便其他模块可以访问它们。在所有
*_info和*_facts模块中实现check_mode。基于事实信息进行条件化的 Playbook 只有在check_mode中返回事实时才能在check_mode下正确条件化。通常,在实例化AnsibleModule时,您可以添加supports_check_mode=True。使用特定于模块的环境变量。例如,如果您使用
module_utils.api中的辅助函数通过module_utils.urls.fetch_url()进行基本身份验证,并且您回退到环境变量以获取默认值,请使用特定于模块的环境变量,如API_<MODULENAME>_USERNAME,以避免模块之间的冲突。保持模块选项简单和专注——如果您正在向现有选项加载大量选择/状态,请考虑添加一个新的、简单的选项。
尽可能保持选项精简。将大型数据结构传递给选项可能会为我们节省一些任务,但这增加了复杂的要求,我们无法在传递给模块之前轻松验证。
如果您想向选项传递复杂数据,请编写一个允许这样做的专家模块,以及几个提供对底层 API 和服务进行“原子”操作的较小模块。复杂操作需要复杂数据。让用户选择是在任务和播放中还是在变量文件中反映这种复杂性。
实现声明式操作(而非 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模块以获取示例。