这次每台机器的 ping
结果都会收集起来,最后统一写到一个文件里,比如 /tmp/ping_results/all.log
。
保存为 ping_baidu.yml
:
- hosts: webservers
become: no
tasks:
- name: Ping www.baidu.com from each host
shell: ping -c 4 www.baidu.com
register: ping_result
- name: Collect results
set_fact:
ping_summary: "{{ ping_summary | default([]) + [inventory_hostname + ':\n' + ping_result.stdout] }}"
- hosts: localhost
gather_facts: no
tasks:
- name: Ensure result directory exists
file:
path: /tmp/ping_results
state: directory
mode: '0755'
- name: Save all results into one file
copy:
content: "{{ hostvars | dict2items | selectattr('value.ping_summary','defined') | map(attribute='value.ping_summary') | sum(start=[]) | join('\n\n') }}"
dest: /tmp/ping_results/all.log
ansible-playbook -i /etc/ansible/hosts/inventory.ini ping_baidu.yml
执行后,控制节点会生成文件:
/tmp/ping_results/all.log
内容类似:
10.0.19.207:
PING www.a.shifen.com (14.215.177.39) 56(84) bytes of data.
64 bytes from 14.215.177.39: icmp_seq=1 ttl=54 time=12.3 ms
...
10.0.19.208:
PING www.a.shifen.com (14.215.177.39) 56(84) bytes of data.
64 bytes from 14.215.177.39: icmp_seq=1 ttl=54 time=15.6 ms
...
Playbook 分成两个 play(部分),每个 play 针对不同的 hosts 执行任务。下面逐部分解释关键命令和逻辑,特别是复杂的 content
表达式,以及为什么它能成功写到文件里。
- hosts: webservers # 针对 inventory 中的 webservers 组的所有主机执行
become: no # 不需要 sudo 权限
tasks:
- name: Ping www.baidu.com from each host
shell: ping -c 4 www.baidu.com # 执行 ping 命令,-c 4 表示发送 4 个包
register: ping_result # 将命令的输出(stdout)注册到变量 ping_result 中
- name: Collect results
set_fact:
ping_summary: "{{ ping_summary | default([]) + [inventory_hostname + ':\n' + ping_result.stdout] }}"
shell 任务:在每台 webservers
主机上运行 ping -c 4 www.baidu.com
,这是标准的 Linux ping 命令,用于测试到 baidu.com 的连通性(发送 4 个 ICMP 包)。输出包括延迟时间、丢包等信息,被存储在 ping_result
变量中(这是一个字典,包含 stdout
字段,即命令的标准输出)。
set_fact 任务:这是核心,用于收集和累积结果。
set_fact
是 Ansible 的内置模块,用于在当前主机上设置一个事实(fact)变量,这个变量是主机特定的(per-host),但 Ansible 会自动将所有主机的 facts 存储在控制节点的 hostvars
(一个全局字典,键是主机名,值是该主机的所有变量和 facts)。ping_summary
是一个列表变量,初始为空(通过 default([])
确保第一次运行时是空列表)。{{ ping_summary | default([]) + [inventory_hostname + ':\n' + ping_result.stdout] }}
:inventory_hostname
是 Ansible 的魔法变量,表示当前主机的 inventory 名称(例如 "10.0.19.207")。ping_result.stdout
是 ping 命令的实际输出字符串(例如 "PING www.a.shifen.com ... 64 bytes from ...")。"10.0.19.207:\nPING ..."
,然后追加到 ping_summary
列表中。ping_summary
列表(只包含自己的结果)。但由于 hostvars
的存在,后续 play 可以访问所有主机的这些列表。这个 play 运行后,不会生成文件,只是在内存中(控制节点的 hostvars
)存储了每个主机的 ping_summary
。
- hosts: localhost # 切换到本地(控制节点)执行,不涉及远程主机
gather_facts: no # 不收集 facts,加速执行
tasks:
- name: Ensure result directory exists
file:
path: /tmp/ping_results
state: directory
mode: '0755' # 创建目录,确保权限可写
- name: Save all results into one file
copy:
content: "{{ hostvars | dict2items | selectattr('value.ping_summary','defined') | map(attribute='value.ping_summary') | sum(start=[]) | join('\n\n') }}"
dest: /tmp/ping_results/all.log # 目标文件路径
file 任务:简单地创建 /tmp/ping_results
目录,如果不存在的话。mode: '0755'
设置权限为 drwxr-xr-x(所有者可读写执行,其他人可读执行)。
copy 任务:这是写入文件的模块。
dest: /tmp/ping_results/all.log
指定了输出文件路径(在控制节点本地)。content
参数是文件的内容,它是一个复杂的 Jinja2 模板表达式(Ansible 使用 Jinja2 处理变量)。这个表达式的作用是从所有主机的 hostvars
中提取、过滤、合并 ping_summary 列表,然后用双换行符连接成一个大字符串,作为文件内容写入。localhost
上,copy
模块会直接在本地文件系统操作,所以能成功写文件(无需远程连接)。现在,重点解释这个 content
表达式的含义和执行逻辑。它看起来复杂,但其实是 Ansible/Jinja2 的链式过滤器(filters)在工作,逐步处理数据。让我一步步拆解:
content
表达式的详细拆解完整表达式:{{ hostvars | dict2items | selectattr('value.ping_summary','defined') | map(attribute='value.ping_summary') | sum(start=[]) | join('\n\n') }}
hostvars:Ansible 的内置魔法变量,是一个字典(dict),包含 playbook 中所有主机的变量和 facts。键是主机名(e.g., "10.0.19.207"),值是该主机的所有变量字典(包括我们设置的 ping_summary
)。例如:
hostvars = {
"10.0.19.207": { "ping_summary": ["10.0.19.207:\nPING ..."] },
"10.0.19.208": { "ping_summary": ["10.0.19.208:\nPING ..."] },
...
}
| dict2items:Jinja2 过滤器,将字典转换成列表 of 字典(每个元素是一个 {"key": 主机名, "value": 该主机的变量字典})。结果:
[
{"key": "10.0.19.207", "value": { "ping_summary": ["10.0.19.207:\nPING ..."] }},
{"key": "10.0.19.208", "value": { "ping_summary": ["10.0.19.208:\nPING ..."] }},
...
]
| selectattr('value.ping_summary','defined'):过滤列表,只保留那些 value.ping_summary
属性已定义(defined)的元素。selectattr
是 Jinja2 过滤器,用于基于对象属性的条件过滤(这里检查 value.ping_summary
是否存在)。如果某个主机没有运行第一个 play 或没有设置这个 fact,它会被过滤掉。结果:只剩有 ping 结果的主机项。
| map(attribute='value.ping_summary'):对过滤后的列表每个元素,提取 value.ping_summary
属性(即每个主机的 ping_summary 列表)。map
过滤器用于从列表中提取/转换属性。结果:一个列表 of 列表,例如:
[
["10.0.19.207:\nPING ..."], # 第一个主机的列表
["10.0.19.208:\nPING ..."], # 第二个主机的列表
...
]
| sum(start=[]):将上面的列表 of 列表扁平化合并成一个单一列表。sum
过滤器在这里用于连接列表(因为 start=[] 指定初始为空列表,它会递归追加所有子列表的内容)。结果:
[
"10.0.19.207:\nPING ...",
"10.0.19.208:\nPING ...",
...
]
| join('\n\n'):将扁平列表中的字符串用 \n\n
(双换行)连接成一个大字符串。结果就是最终的文件内容:
10.0.19.207:
PING www.a.shifen.com ...
10.0.19.208:
PING www.a.shifen.com ...
/tmp/ping_results/all.log
在控制节点(localhost)的本地文件系统。/tmp
目录通常对所有用户可写(权限 1777),所以 Ansible 在本地运行 copy
模块时,能直接创建/写入文件,无需额外权限。copy
模块的 content
参数允许直接指定字符串内容(而不是从源文件复制)。当 hosts: localhost
时,它使用本地连接(local connection),相当于在控制节点 shell 中执行 echo "content" > file
,非常高效。set_fact
将结果存入 hostvars
(Ansible 内部全局存储),第二个 play 能访问它。即使主机并行执行,hostvars
也会在 play 间持久化。selectattr('defined')
会自动过滤掉无效结果,确保文件只包含成功的主机数据。