最近在写部署相关代码时需要使用 Ansible,于是研究了一下如何写 Ansible Collections。在实践过程中需要在 Ansible 模块中访问 Ansible Facts 来做一些条件判断,但是在官方开发文档中没有发现任何相关说明,搜索到的 Stackoverflow 上的回答也比较违反常理,所以自己看了一下 Ansible 的源码,找到了一个比较正常的方式。

Ansible 内置的 Facts 模块

既然要从模块里调用,那第一反应自然是直接调用 Ansible 里和 Facts 相关的 API。浏览代码库可知 Fact 相关的代码在 lib/ansible/module_utils/facts 目录下,比如我想获取操作系统信息,可以在自己的模块中导入 lib/ansible/module_utils/facts/system/distribution.py 中的 Distribution 类,然后调用相关方法即可。

from ansible.module_utils.facts.system.distribution import Distribution

distribution = Distribution(module=module)
distro_facts = distribution.get_distribution_facts()

但并不是每一种 Fact 都有类似 Distribution 这样的 Class 可以直接调用,更何况我们更希望一次获取到所有自己需要的 Fact 字典,而不是每个 Fact 都自己写个实现。

Setup 模块 和 FactCollector 类

深入调查得知,Fact 获取是通过一大波 FactCollector 类实现的。首先有一个 BaseFactCollector 基类,每一组 Fact 获取都是 BaseFactCollector 的实现,最后 Ansible 通过 ansible.builtin.setup 模块(lib/ansible/modules/setup.py)来执行和输出。

ansible.builtin.setup 模块中创建 Collectors 和获取 Facts 的方式如下。

fact_collector = \
    ansible_collector.get_ansible_collector(all_collector_classes=all_collector_classes,
                                            namespace=namespace,
                                            filter_spec=filter_spec,
                                            gather_subset=gather_subset,
                                            gather_timeout=gather_timeout,
                                            minimal_gather_subset=minimal_gather_subset)

facts_dict = fact_collector.collect(module=module)

既然如此,那我们搞清楚 get_ansible_collector 方法各项参数的意义,然后在自己的模块里模仿一下就 OK 了。

如何在自己的模块里使用 Fact Collector

Ansible 所有的 FactCollector 可以在 lib/ansible/module_utils/facts/default_collectors.py 中找到,从对应的源文件中可以大致了解各自获取的目标 Fact。

ansible.builtin.setup 模块默认会执行所有符合目标 Platform 的 Collector,但是可以通过 gather_subset 参数来指定需要的 Collector 名,另外还能使用 filter_spec 参数来过滤结果。很多 Collector 并非调用系统 API,而是直接使用 Shell 脚本等方式来获取信息的,所以为了执行速度,我们最好仅仅指定那些自己需要的 Collector。

比如我只需要获取 主机名、操作系统、网络 这三类信息,可以指定 gather_subset 参数。

collectors = default_collectors._base + default_collectors._network
gather_subset = ['platform', 'distribution', 'network']

fact_collector = \
    ansible_collector.get_ansible_collector(all_collector_classes=collectors,
                                            gather_subset=gather_subset)

facts = fact_collector.collect(module=module)

这样 Ansible 在执行这个模块时就只会获取这三种 Fact,然后就可以随意使用这些数据了。事实上,上述的 all_collector_classes 参数也可以直接使用默认的 Collectors(default_collectors.collectors),无需特别指定,不过限定之后可以让程序在匹配 Subset 时少跑几个循环,可能略微快那么一丢丢。

后话

作为一个自动化运维框架,「在第三方模块里直接有效地获取系统信息」这种需求应该是很基本的,照理官方文档会提及才对。不过就我这次的体验看,以 Ansible 的开发文档之杂乱,出现这种重要信息的缺失也是情理之中。比如官方文档中开发环境配置的第一步竟然是 Clone 整个 Ansible Repo,简直不敢相信,我是要写自己的 Module,不是要搞 Ansible 开发啊,何至于。