Ansible写Python自定义模块的基本流程和注意事项?

文章导读
想给 Ansible 补一个现有模块覆盖不到的操作,大多数人的第一反应是写个脚本,然后放到远端执行。这个思路可以,但 Ansible 模块不是普通脚本,它要遵守两条约定:参数从 stdin 读,结果以 JSON 写到 stdout。把这两条弄明白,再配合一小段测试命令,后续调整就有方向了。
📋 目录
  1. A 先把入口条件理顺
  2. B 参数类型和必填项要先声明
  3. C 返回结果只留一个 JSON 出口
  4. D 幂等性写在判断条件里
  5. E 本地调试和远程保留临时文件
A A

想给 Ansible 补一个现有模块覆盖不到的操作,大多数人的第一反应是写个脚本,然后放到远端执行。这个思路可以,但 Ansible 模块不是普通脚本,它要遵守两条约定:参数从 stdin 读,结果以 JSON 写到 stdout。把这两条弄明白,再配合一小段测试命令,后续调整就有方向了。

先把入口条件理顺

自定义模块本质上是一个可执行脚本,Ansible通过调用该脚本的stdin传入参数。在Python模块中,通常以`if __name__ == '__main__':`作为入口,然后实例化`AnsibleModule`对象。判断依据是:如果直接执行脚本,参数会从stdin读取JSON;如果通过`-a`选项传入,需要确保模块支持。实际操作时,建议在模块开头导入`ansible.module_utils.basic`,并检查`sys.stdin`是否为空,避免因无输入导致崩溃。

入口这里有一个容易忽略的点:Ansible 执行模块时,stdin 传入的 JSON 结构并不是业务参数本身,而是包含 `ANSIBLE_MODULE_ARGS` 键的包装对象。手动测试时不要直接传 `{"path": "/tmp/test"}`,否则 `AnsibleModule` 可能拿不到参数。稳妥的做法是先调用 `AnsibleModule` 解析参数。手动测试的输入文件要写成 `{"ANSIBLE_MODULE_ARGS": {"path": "/tmp/test"}}` 这种结构,再用 `python module.py args.json` 去跑。确认模块能在本地解析参数后,再回到 playbook 里看调试输出。

参数类型和必填项要先声明

定义参数时需使用`AnsibleModule(argument_spec=...)`,每个参数可以指定`required`、`type`、`choices`等属性。常见坑是忘记声明`required=True`导致参数缺失时模块仍运行,最终出现TypeError。另一个坑是类型设置错误,例如布尔值用`type='bool'`,字符串用`type='str'`;若不声明,默认类型是字符串,可能导致数值比较出错。建议在模块开头打印参数结果到临时文件,便于调试。

`argument_spec` 的好处是校验逻辑前置。例如参数声明了 `choices: [start, stop, restart]`,用户传一个 `reload`,Ansible 会在调用模块前直接返回失败,模块代码里不需要写一堆 if。参数类型也要假设得窄一些:路径类参数用 `type='path'`,既兼容绝对路径也处理相对路径;枚举值用 `choices` 加默认值,避免空值判断。调试时如果想打印参数,不要直接 `print`,建议写到 `/tmp/module_args.log`,任务跑完去目标节点看这个文件,看完删掉。

Ansible写Python自定义模块的基本流程和注意事项?

返回结果只留一个 JSON 出口

模块必须输出一个JSON字符串作为返回结果,否则Ansible会报错。正确做法是使用`module.exit_json(changed=...)`或`module.fail_json(msg=...)`。注意:不要在模块中直接`print`非JSON内容,否则会被Ansible解释为失败。常见的错误包括混用`print`和`exit_json`,导致输出中混入无关字符。检查方法是在本地命令行运行模块,传入测试参数,观察stdout是否为标准JSON格式。

`exit_json` 和 `fail_json` 本质都是抛出 `SystemExit`,所以调用之后模块会立即终止。不要在 `exit_json` 之后还写清理逻辑,也不要尝试捕获那个异常。验证返回结果最简单的命令是 `python /path/to/module.py /tmp/args.json | python -m json.tool`,如果第二段命令能正常格式化,说明 stdout 里没有额外字面量;如果报错,就需要回看模块里是否还有调试 `print` 或库代码输出了日志。

幂等性写在判断条件里

自定义模块最容易让后续维护头疼的问题,是每次执行都报告 `changed`。因为 Ansible 会依赖返回值里的 `changed` 字段决定是否触发 handler,不是看实际操作。写模块时建议先检查目标状态,只有在状态不一致时才做变更。例如写文件前,先判断文件是否存在、内容是否与期望一致;如果一致,直接 `module.exit_json(changed=False)`,否则才打开文件写入,并返回 `changed=True`。这个判断要放在 `try/except` 外面或至少捕获具体异常,用 `module.fail_json(msg=..., exception=traceback.format_exc())` 传递完整信息,不要用裸 `except` 把错误吞掉。

边界同样要看清楚:如果操作系统本身没有提供查询当前状态的命令,就不必强行实现幂等,可以在模块说明里声明“每次都会执行并返回 changed”,由上面的 play 任务用 `changed_when` 覆盖。但这类例外越少越好。

Ansible写Python自定义模块的基本流程和注意事项?

本地调试和远程保留临时文件

调试自定义模块最直接的方式,是在本地人工造一个参数文件,内容要符合 Ansible 的包装结构:{"ANSIBLE_MODULE_ARGS": {"path": "/tmp/test", "content": "hello"}},然后运行:

python /path/to/module.py /tmp/args.json

如果模块依赖系统库或某个命令,本地有但目标节点没有,需要提前在目标节点上验证。另一个实用检查点是设置环境变量执行任务:

ANSIBLE_KEEP_REMOTE_FILES=1 ansible-playbook -i hosts check_module.yml -vvv

执行结束后去远程节点的 `~/.ansible/tmp/` 找到保留的模块文件,可以直接在远端手工执行,做差异对比。Ansible 在远程执行时会释放同一份 Python 代码,但运行目录和当前用户可能跟本地不同,所以模块里尽量避免依赖 `~` 或 `os.getcwd()`,改用绝对路径或通过 `os.path.expanduser` 显式处理。

整体来说,写 Ansible 自定义模块最需要先确认的是参数入口、返回格式和幂等条件,其次才是具体操作逻辑。如果你在调试中卡住,优先做两件事:一是在本地用 `args.json` 跑一遍,检查 stdout 能不能被 `json.tool` 解析;二是用 `ANSIBLE_KEEP_REMOTE_FILES=1` 保留远程临时文件,看实际执行环境缺什么。把这两步做完,大多数无名报错都能定位到具体行。