操纵数据
在许多情况下,您需要对变量进行复杂的操作。虽然不建议将 Ansible 作为数据处理/操纵工具,但您可以将现有的 Jinja2 模板与许多新增的 Ansible 过滤器(filters)、查找(lookups)和测试(tests)结合使用,以执行非常复杂的转换。
- 让我们先快速定义每种插件的类型:
lookups(查找):主要用于查询“外部数据”。在 Ansible 中,它们曾是使用
with_<lookup>结构循环的主要部分,但它们也可以独立使用以返回数据进行处理。由于之前提到的在循环中的主要功能,它们通常返回一个列表。使用 Jinja2 的lookup或query运算符调用。filters(过滤器):用于更改/转换数据,使用 Jinja2 的
|运算符调用。tests(测试):用于验证数据,使用 Jinja2 的
is运算符调用。
循环和列表推导式
大多数编程语言都有循环(for, while 等)和列表推导式,用于对列表(包括对象列表)进行转换。Jinja2 提供了几个具有此类功能的过滤器:map, select, reject, selectattr, rejectattr。
map:这是一个基础的 for 循环,允许您更改列表中的每一项;使用 “attribute” 关键字,您可以基于列表元素的属性进行转换。
select/reject:这是一个带有条件的 for 循环,允许您根据条件的执行结果创建列表的子集(匹配或不匹配)。
selectattr/rejectattr:与上述非常相似,但它在条件语句中使用列表元素的特定属性。
使用循环创建指数级退避(exponential backoff)。
- name: try wait_for_connection up to 10 times with exponential delay
ansible.builtin.wait_for_connection:
delay: '{{ item | int }}'
timeout: 1
loop: '{{ range(1, 11) | map("pow", 2) }}'
loop_control:
extended: true
ignore_errors: "{{ not ansible_loop.last }}"
register: result
when: result is not defined or result is failed
从字典中提取与列表元素匹配的键
等效的 Python 代码为:
chains = [1, 2]
for chain in chains:
for config in chains_config[chain]['configs']:
print(config['type'])
在 Ansible 中有多种方法可以实现,这只是其中一个示例:
tasks:
- name: Show extracted list of keys from a list of dictionaries
ansible.builtin.debug:
msg: "{{ chains | map('extract', chains_config) | map(attribute='configs') | flatten | map(attribute='type') | flatten }}"
vars:
chains: [1, 2]
chains_config:
1:
foo: bar
configs:
- type: routed
version: 0.1
- type: bridged
version: 0.2
2:
foo: baz
configs:
- type: routed
version: 1.0
- type: bridged
version: 1.1
ok: [localhost] => {
"msg": [
"routed",
"bridged",
"routed",
"bridged"
]
}
vars:
unique_value_list: "{{ groups['all'] | map ('extract', hostvars, 'varname') | list | unique}}"
查找挂载点
在这种情况下,我们想在机器中为给定路径查找挂载点。由于我们已经收集了挂载事实(mount facts),我们可以使用以下方法:
- hosts: all
gather_facts: True
vars:
path: /var/lib/cache
tasks:
- name: The mount point for {{path}}, found using the Ansible mount facts, [-1] is the same as the 'last' filter
ansible.builtin.debug:
msg: "{{(ansible_facts.mounts | selectattr('mount', 'in', path) | list | sort(attribute='mount'))[-1]['mount']}}"
组合来自同一字典列表的值
结合上述示例中的正向和反向过滤器,您可以实现“存在时取值”以及不存在时的“回退(fallback)”方案。
- hosts: localhost
tasks:
- name: Check hosts in inventory that respond to ssh port
wait_for:
host: "{{ item }}"
port: 22
loop: '{{ has_ah + no_ah }}'
vars:
has_ah: '{{ hostvars|dictsort|selectattr("1.ansible_host", "defined")|map(attribute="1.ansible_host")|list }}'
no_ah: '{{ hostvars|dictsort|rejectattr("1.ansible_host", "defined")|map(attribute="0")|list }}'
基于变量的自定义 Fileglob
本示例使用 Python 参数列表解包(unpacking) 来根据变量创建自定义的 fileglob 列表。
- hosts: all
vars:
mygroups:
- prod
- web
tasks:
- name: Copy a glob of files based on a list of groups
copy:
src: "{{ item }}"
dest: "/tmp/{{ item }}"
loop: '{{ q("fileglob", *globlist) }}'
vars:
globlist: '{{ mygroups | map("regex_replace", "^(.*)$", "files/\1/*.conf") | list }}'
复杂类型转换
Jinja 提供了用于简单数据类型转换的过滤器(int, bool 等),但当您想要转换数据结构时,事情就不那么简单了。您可以像上面那样使用循环和列表推导式来辅助,其他过滤器和查找也可以链式调用以实现更复杂的转换。
从列表创建字典
在大多数语言中,从一对对的列表创建字典(也称为 map/关联数组/hash 等)很容易。在 Ansible 中有几种方法可以实现,最适合您的方法可能取决于您的数据来源。
这些示例将生成 {"a": "b", "c": "d"}
vars:
single_list: [ 'a', 'b', 'c', 'd' ]
mydict: "{{ dict(single_list[::2] | zip_longest(single_list[1::2])) }}"
vars:
list_of_pairs: [ ['a', 'b'], ['c', 'd'] ]
mydict: "{{ dict(list_of_pairs) }}"
两者最终结果相同,其中 zip_longest 将 single_list 转换为 list_of_pairs 生成器。
稍微复杂一点的情况:使用 set_fact 和 loop,利用两个列表中的键值对来创建/更新字典。
- name: Uses 'combine' to update the dictionary and 'zip' to make pairs of both lists
ansible.builtin.set_fact:
mydict: "{{ mydict | default({}) | combine({item[0]: item[1]}) }}"
loop: "{{ (keys | zip(values)) | list }}"
vars:
keys:
- foo
- var
- bar
values:
- a
- b
- c
这将导致结果为 {"foo": "a", "var": "b", "bar": "c"}。
您甚至可以将这些简单示例与其他过滤器和查找结合,通过将模式与变量名匹配来动态创建字典。
vars:
xyz_stuff: 1234
xyz_morestuff: 567
myvarnames: "{{ q('varnames', '^xyz_') }}"
mydict: "{{ dict(myvarnames|map('regex_replace', '^xyz_', '')|list | zip(q('vars', *myvarnames))) }}"
快速解释一下,因为这两行代码包含很多内容:
varnames查找会返回一个匹配“以xyz_开头”的变量列表。然后将上一步得到的列表传递给
vars查找以获取值列表。*用于“解引用列表”(这是一个在 Jinja 中可行的 Python 习惯用法),否则它会将整个列表作为一个单一参数处理。这两个列表都被传递给
zip过滤器,将它们配对成一个统一的列表(键, 值, 键2, 值2, …)。然后 dict 函数接收这个“键值对列表”来创建字典。
一个如何使用 facts 来查找满足条件 X 的主机数据的示例。
vars:
uptime_of_host_most_recently_rebooted: "{{ansible_play_hosts_all | map('extract', hostvars, 'ansible_uptime_seconds') | sort | first}}"
一个显示主机运行时间(天/小时/分钟/秒)的示例(假设已收集 facts)。
- name: Show the uptime in days/hours/minutes/seconds
ansible.builtin.debug:
msg: Uptime {{ now().replace(microsecond=0) - now().fromtimestamp(now(fmt='%s') | int - ansible_uptime_seconds) }}