Ansible把所有节点的结果汇总成一个文件(而不是分开多个文件)

半兽人 发表于: 2025-09-12   最后更新时间: 2025-09-12 18:04:55  
{{totalSubscript}} 订阅, 30 游览

这次每台机器的 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') 会自动过滤掉无效结果,确保文件只包含成功的主机数据。
更新于 2025-09-12

查看Ansible更多相关的文章或提一个关于Ansible的问题,也可以与我们一起分享文章