这次每台机器的 ping
结果都会收集起来,最后统一写到一个文件里,比如 /tmp/ping_results/all.log
。
完整 Playbook 示例
保存为 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
表达式,以及为什么它能成功写到文件里。
第一个 Play:针对远程主机收集 ping 结果
- 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
。
第二个 Play:针对控制节点(localhost)汇总并写入文件
- 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 列表,然后用双换行符连接成一个大字符串,作为文件内容写入。- 因为这个 play 运行在
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 模块的行为:
copy
模块的content
参数允许直接指定字符串内容(而不是从源文件复制)。当hosts: localhost
时,它使用本地连接(local connection),相当于在控制节点 shell 中执行echo "content" > file
,非常高效。 - 数据来源可靠:因为第一个 play 的
set_fact
将结果存入hostvars
(Ansible 内部全局存储),第二个 play 能访问它。即使主机并行执行,hostvars
也会在 play 间持久化。 - 潜在问题避免:如果某些主机失败,
selectattr('defined')
会自动过滤掉无效结果,确保文件只包含成功的主机数据。