把当初用 shell 凑合的事情做成插件
目标
亲手做出 Jinja2 filter、test、lookup 和自定义模块,并在 playbook 中调用。通过代码确认这四种分别在哪里运行,以及这种位置的差别让什么能做、让什么不能做。
为什么重要
playbook 一变大,哪个团队都会出现同样的位置——整理名称、给数字分类、到某处查一张表。如果标准 filter 做不到,人们就用 shell 和 sed 凑合,从那一刻起,那个 task 就失去了幂等性和检查模式。插件就是为这个位置准备的。不过不是随便放在哪里都行,在哪里运行是设计的一半。filter、test、lookup 在控制节点上运行,所以不需要在目标主机上安装任何东西,但看不到目标主机的状态;模块被复制到目标主机并在那里运行,所以可以改变状态,但必须自己负责幂等性和检查模式。本实验在这条边界上来回跨越四次,亲手确认。
步骤
- 创建
/root/ansplug/ansible.cfg,把默认 inventory 指定为./inventory/hosts.ini。在/root/ansplug/inventory/hosts.ini中写三个组——web中有web1(svc_port 8080,svc_nameWeb Front 01)和web2(9090,Web Front 02),db中有db1(5432,Main DB!),edge中有cache1(443,Edge Cache)。四台主机都是ansible_host=127.0.0.1ansible_port=2222,[all:vars]中的ansible_user是root。然后运行ansible all -m ansible.builtin.ping -o,把标准输出和标准错误一起保存到/root/ansplug/out/ping.txt。 - 创建
/root/ansplug/filter_plugins/labfilters.py,注册一个slugifyfilter。这个 filter 把字符串转为小写,把所有非字母数字的字符都换成-,把连续出现的-合并成一个,并去掉两端的-。如果传入的不是字符串,就抛出AnsibleFilterError。然后创建/root/ansplug/slug.yml,把'Web Server 01!!'交给这个 filter 得到的结果写到/root/ansplug/out/slug.txt,并运行。 - 在同一个
/root/ansplug/filter_plugins/labfilters.py中再加一个port_classfilter。port_class(value, privileged_below=1024, dynamic_from=49152)在端口小于privileged_below时返回system,大于等于dynamic_from时返回dynamic,介于两者之间时返回user,如果不是整数就抛出AnsibleFilterError。然后创建/root/ansplug/ports.yml,把 inventory 的四台主机按名称升序,每行一台以<호스트> <포트> <분류>(占位符依次为主机、端口、分类)写到/root/ansplug/out/ports.txt,并运行。 - 创建
/root/ansplug/test_plugins/labtests.py,注册reserved_porttest——端口号小于 1024 时为真。然后创建/root/ansplug/reserved.yml,把四台主机按名称升序,每行一台以<호스트> <포트> <reserved|free>(占位符依次为主机、端口、reserved 或 free)写到/root/ansplug/out/reserved.txt,并运行。判定必须以is reserved_port的形式调用。 - 在
/root/ansplug/registry.json中写入{"web": "seoul-a", "db": "seoul-b", "edge": "seoul-c"}。创建/root/ansplug/lookup_plugins/labregistry.py,定义labregistrylookup——接收一个键并返回注册表中的值,可以用registry=关键字参数指定其他文件(默认值是/root/ansplug/registry.json),文件不存在或键不存在时抛出AnsibleError。然后用/root/ansplug/regions.yml把db、edge、web三个组按这个顺序以<그룹> <지역>(占位符依次为组、区域)写到/root/ansplug/out/regions.txt,并运行。 - 在
/root/ansplug/collections/ansible_collections/labhub/site/中放置 collection——galaxy.yml的namespace是labhub,name是site,filter 文件放在plugins/filter/下(把第 2、3 步做好的原样复制过来即可)。在/root/ansplug/ansible.cfg中加上collections_path = ./collections,用/root/ansplug/fqcn.yml把'Prod DB 02!!'交给labhub.site.slugify得到的结果写到/root/ansplug/out/fqcn.txt,并运行。 - 在
/root/ansplug/library/lab_marker.py中创建自定义模块lab_marker——参数是path和content,如果文件内容已经相同就以changed=false结束,不同就写入文件并以changed=true结束。声明supports_check_mode=True,并且在检查模式下不写入。用/root/ansplug/marker.yml对web组运行这个模块,让它向/root/ansplug/out/<호스트이름>.marker(占位符为主机名)写入那台主机的名称一行,把 playbook 运行两次,把第二次运行的输出保存到/root/ansplug/out/marker_run2.txt。 - 用
/root/ansplug/report.yml把四台主机按名称升序每行一台写到/root/ansplug/out/report.txt——格式是<호스트> <이름슬러그> <포트> <포트분류> <reserved|free> <지역>(占位符依次为主机、名称 slug、端口、端口分类、reserved 或 free、区域),名称 slug 是对svc_name做slugify的值,端口分类是port_class,第五栏是reserved_porttest,区域是用labregistry查询该主机第一个组名得到的值。把 playbook 运行两次,并把第二次运行的输出保存到/root/ansplug/out/report_run2.txt。
参考
filter_plugins/、test_plugins/、lookup_plugins/、library/必须在 playbook 旁边才能被找到。所以本实验的 playbook 全都放在/root/ansplug正下方。- sshd 运行在 127.0.0.1 的 2222 端口上。inventory 中的四台主机只是名称不同,全都连到同一台服务器。
- 调用方式因类型而异——filter 是
값 | 이름,test 是값 is 이름,lookup 是lookup('이름', 인자)(占位符依次为值、名称、参数),模块是 task 的键。 - 常见错误:把类名起成了不是
FilterModule的名字,然后纳闷“怎么找不到”。文件名可以随意,类名是约定。 - 常见错误:用 ad-hoc 命令试新 filter 后,就判断它不能用——旁边目录的查找只对 playbook 生效。
- 常见错误:放置 collection 时缺少
ansible_collections/这一层。不会报错,只是找不到。 - Developing plugins · Adding modules and plugins locally · Lookup plugins · Collection structure · Developing modules
先搭起模拟四台主机的 inventory
创建 /root/ansplug/ansible.cfg,把默认 inventory 指定为 ./inventory/hosts.ini。在 /root/ansplug/inventory/hosts.ini 中写三个组——web 中有 web1(svc_port 8080,svc_name Web Front 01)和 web2(9090,Web Front 02),db 中有 db1(5432,Main DB!),edge 中有 cache1(443,Edge Cache)。四台主机都是 ansible_host=127.0.0.1 ansible_port=2222,[all:vars] 中的 ansible_user 是 root。然后运行 ansible all -m ansible.builtin.ping -o,把标准输出和标准错误一起保存到 /root/ansplug/out/ping.txt。
这个 Pod 的 sshd 运行在 127.0.0.1 的 2222 端口上,并且可以密钥认证。在 inventory 中写多个名称,也全都连到同一个 sshd,所以可以模拟“多台”。在 INI inventory 中,值里有空格就用双引号括起来。合并后的结果用 ansible-inventory --list 确认。
第一个 filter——在控制节点上运行的纯函数
创建 /root/ansplug/filter_plugins/labfilters.py,注册一个 slugify filter。这个 filter 把字符串转为小写,把所有非字母数字的字符都换成 -,把连续出现的 - 合并成一个,并去掉两端的 -。如果传入的不是字符串,就抛出 AnsibleFilterError。然后创建 /root/ansplug/slug.yml,把 'Web Server 01!!' 交给这个 filter 得到的结果写到 /root/ansplug/out/slug.txt,并运行。
Ansible 会在 filter_plugins/ 内的 Python 文件中查找名为 FilterModule 的类,并把该类的 filters() 所返回字典的键用作 filter 名称。这个目录必须在 playbook 旁边才能被找到——在 ad-hoc 命令中找不到。filter 在控制节点上运行,所以不需要在目标主机上安装任何东西,也因此更要是没有副作用的纯函数。
接收参数的 filter,以及截断错误输入的位置
在同一个 /root/ansplug/filter_plugins/labfilters.py 中再加一个 port_class filter。port_class(value, privileged_below=1024, dynamic_from=49152) 在端口小于 privileged_below 时返回 system,大于等于 dynamic_from 时返回 dynamic,介于两者之间时返回 user,如果不是整数就抛出 AnsibleFilterError。然后创建 /root/ansplug/ports.yml,把 inventory 的四台主机按名称升序,每行一台以 <호스트> <포트> <분류>(占位符依次为主机、端口、分类)写到 /root/ansplug/out/ports.txt,并运行。
Jinja2 filter 把管道左边的值作为第一个参数,括号里写的是后面的参数——就像 {{ 3000 | port_class(2000, 4000) }} 这样。把默认值放在 Python 一侧,大多数调用不带参数就结束,只有特殊的地方才改变边界来调用。在 Python 中 True 是 int 的子类型,会通过 isinstance(x, int)——必须先把布尔值筛掉。遍历所有主机用的是 groups['all'] 和 hostvars[...]。
test 插件——只返回真假的位置
创建 /root/ansplug/test_plugins/labtests.py,注册 reserved_port test——端口号小于 1024 时为真。然后创建 /root/ansplug/reserved.yml,把四台主机按名称升序,每行一台以 <호스트> <포트> <reserved|free>(占位符依次为主机、端口、reserved 或 free)写到 /root/ansplug/out/reserved.txt,并运行。判定必须以 is reserved_port 的形式调用。
test 与 filter 的类名和方法名都不同——是 TestModule 和 tests()。目录也是 test_plugins/。调用方式也不同。filter 是 값 | 이름,test 是 값 is 이름(占位符依次为值、名称),而且 test 必须只返回真或假。之所以有这个区分,是因为 when: 和 select、reject 是以 test 为前提来使用的——返回不是真假的值,含义当场就会崩塌。
lookup——读取控制节点文件的位置
在 /root/ansplug/registry.json 中写入 {"web": "seoul-a", "db": "seoul-b", "edge": "seoul-c"}。创建 /root/ansplug/lookup_plugins/labregistry.py,定义 labregistry lookup——接收一个键并返回注册表中的值,可以用 registry= 关键字参数指定其他文件(默认值是 /root/ansplug/registry.json),文件不存在或键不存在时抛出 AnsibleError。然后用 /root/ansplug/regions.yml 把 db、edge、web 三个组按这个顺序以 <그룹> <지역>(占位符依次为组、区域)写到 /root/ansplug/out/regions.txt,并运行。
lookup 是继承 LookupBase 的 LookupModule,入口是 run(self, terms, variables=None, **kwargs)。terms 是 lookup('이름', 첫째, 둘째)(占位符依次为名称、第一个、第二个)的参数列表,必须返回列表。通过 kwargs 传来的是 registry= 这样的关键字参数。重要的是这段代码在控制节点上运行——lookup 读取的文件必须在运行 playbook 的位置,而不是目标服务器上。
把同一个 filter 迁入 collection,并用 FQCN 调用
在 /root/ansplug/collections/ansible_collections/labhub/site/ 中放置 collection——galaxy.yml 的 namespace 是 labhub,name 是 site,filter 文件放在 plugins/filter/ 下(把第 2、3 步做好的原样复制过来即可)。在 /root/ansplug/ansible.cfg 中加上 collections_path = ./collections,用 /root/ansplug/fqcn.yml 把 'Prod DB 02!!' 交给 labhub.site.slugify 得到的结果写到 /root/ansplug/out/fqcn.txt,并运行。
collection 的位置固定为 <검색경로>/ansible_collections/<네임스페이스>/<이름>/(占位符依次为搜索路径、命名空间、名称)——这三层只要错开一层,就会不报任何错误地找不到。在 collection 内,filter 的位置不是 filter_plugins/ 而是 plugins/filter/。test 是 plugins/test/,lookup 是 plugins/lookup/,模块是 plugins/modules/。是否正确找到,用 ansible-config dump | grep COLLECTIONS 确认。
模块在目标主机上运行——幂等性和检查模式由它自己负责
在 /root/ansplug/library/lab_marker.py 中创建自定义模块 lab_marker——参数是 path 和 content,如果文件内容已经相同就以 changed=false 结束,不同就写入文件并以 changed=true 结束。声明 supports_check_mode=True,并且在检查模式下不写入。用 /root/ansplug/marker.yml 对 web 组运行这个模块,让它向 /root/ansplug/out/<호스트이름>.marker(占位符为主机名)写入那台主机的名称一行,把 playbook 运行两次,把第二次运行的输出保存到 /root/ansplug/out/marker_run2.txt。
与 filter、test、lookup 不同,模块会被复制到目标主机并在那里执行。所以读不到控制节点的文件,但可以改变状态——能改变意味着必须自己负责幂等性和检查模式。模块的骨架是用 AnsibleModule(argument_spec=..., supports_check_mode=True) 接收参数,用 module.exit_json(changed=...) 结束。如果漏掉 supports_check_mode,在 --check 中那个 task 会被直接跳过——不是失败而是沉默,所以更危险。模块在 playbook 旁边的 library/ 中查找。
在一个 playbook 中把四种串起来
用 /root/ansplug/report.yml 把四台主机按名称升序每行一台写到 /root/ansplug/out/report.txt——格式是 <호스트> <이름슬러그> <포트> <포트분류> <reserved|free> <지역>(占位符依次为主机、名称 slug、端口、端口分类、reserved 或 free、区域),名称 slug 是对 svc_name 做 slugify 的值,端口分类是 port_class,第五栏是 reserved_port test,区域是用 labregistry 查询该主机第一个组名得到的值。把 playbook 运行两次,并把第二次运行的输出保存到 /root/ansplug/out/report_run2.txt。
一台主机所属的组名在 hostvars[h].group_names 里,这里它的第一个元素就是注册表的键。把 Jinja2 的 {% for %} 块原样放进 copy 模块的 content,可以一次生成多行。输入相同则文件内容相同,所以第二次运行必须是 changed=0——这就是“生成报告的 playbook”具备幂等性的证据。