使用 Ansible + Netstat 批量采集外部连接并导出 Excel 报表(含空主机)

使用 Ansible + Netstat 批量采集外部连接并导出 Excel 报表(含空主机)

 次点击
24 分钟阅读

在多节点运维场景中,我们经常需要分析主机与外部网络的连接情况。
本文介绍一种方法,通过 Ansible + Netstat + OpenPyXL 批量收集受控主机的外部连接(仅保留特定网段),并将结果导出为 Excel 报表。

特点如下:

  • 🔍 基于 netstat,兼容老版本系统;

  • ✅ 自动识别 IPv6 映射地址(如 ::ffff:192.168.1.1);

  • 📊 输出格式清晰:主机 IP、外部 IP 数量、外部 IP 列表;

  • 📄 即使某台主机无连接,也会显示在报表中;

  • 🧩 纯 Ansible 实现,无需人工拼 JSON。


一、场景背景

在日常巡检或安全审计中,常需要了解每台主机当前的外部连接情况,例如哪些 IP 与目标网段(如 100.*)有交互。
传统做法是逐台登录主机执行 netstat,再人工整理结果,这样效率低且容易出错。

本文提供一个更自动化的方案:

  • 使用 Ansible 在批量主机上执行 netstat

  • 提取特定网段的连接;

  • 在控制节点上汇总、去重;

  • 最终生成带合并单元格的 .xlsx 报表。


二、实现思路

  1. 采集层:在每台主机执行 netstat -ntu,抓取所有已建立的连接;

  2. 过滤层:仅保留符合条件的外部 IPv4 地址(例如 100.*),并过滤掉已知例外;

  3. 汇总层:Ansible 在控制端汇总所有主机结果,转为 JSON;

  4. 展示层:使用 openpyxl 将 JSON 渲染为 Excel 文件,自动合并单元格。

Excel 格式如下:

受控主机IP

外部链接唯一IP个数

外部链接IP(逐行)

10.0.1.10

5

100.58.230.144
100.58.70.170

10.0.1.20

0

(空)


三、核心命令逻辑

netstat 输出示例:

Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 10.0.1.10:5000          100.58.70.170:1556      ESTABLISHED
tcp        0      0 10.0.1.10:4200          ::ffff:100.58.230.144:53217 ESTABLISHED

可以看到有 IPv6 映射格式 ::ffff:100.58.230.144:53217
我们只需提取其中的 IPv4 部分。

过滤命令如下:

ansible.builtin.shell: |
  set -o pipefail
  EXCLUDE_RE="{{ excluded_ips | map('regex_escape') | join('|') }}"
  netstat -ntu 2>/dev/null \
  | awk 'NR>2 && ($6=="ESTABLISHED" || $6=="ESTAB"){print $5}' \
  | sed -E 's/^::ffff:([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+):[0-9]+$/\1/; s/:([0-9]+)$//' \
  | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' \
  | grep -v -E "^($EXCLUDE_RE)$" \
  | grep -E '^100\.' \
  | sort -u || true

四、完整 Playbook 示例

---
- name: Collect external connections
  hosts: all
  gather_facts: no
  vars:
    excluded_ips:
      - 127.0.0.7

  tasks:
    - name: Collect established peer IPv4 via netstat
      ansible.builtin.shell: |
        set -o pipefail
        EXCLUDE_RE="{{ excluded_ips | map('regex_escape') | join('|') }}"
        netstat -ntu 2>/dev/null \
        | awk 'NR>2 && ($6=="ESTABLISHED" || $6=="ESTAB"){print $5}' \
        | sed -E 's/^::ffff:([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+):[0-9]+$/\1/; s/:([0-9]+)$//' \
        | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' \
        | grep -v -E "^($EXCLUDE_RE)$" \
        | grep -E '^100\.' \
        | sort -u || true
      args:
        executable: /bin/bash
      register: ext_ips_cmd
      changed_when: false

    - name: Save facts (include empty)
      ansible.builtin.set_fact:
        ext_ips: "{{ ext_ips_cmd.stdout_lines | default([]) }}"
        ext_ip_count: "{{ (ext_ips_cmd.stdout_lines | default([])) | length }}"
        host_display_ip: "{{ hostvars[inventory_hostname].ansible_host | default(inventory_hostname) }}"

- name: Build Excel report on controller
  hosts: localhost
  gather_facts: no
  vars:
    json_path: "./external_links.json"
    xlsx_path: "./external_links.xlsx"
  tasks:
    - name: Combine results
      ansible.builtin.set_fact:
        collected: >-
          {{ groups['all'] | map('extract', hostvars, ['host_display_ip', 'ext_ip_count', 'ext_ips'])
             | map('combine', [{'host': None, 'count': None, 'ips': None}])
             | list }}

    - name: Write JSON
      ansible.builtin.copy:
        dest: "{{ json_path }}"
        mode: '0644'
        content: "{{ collected | to_nice_json }}"

    - name: Generate Excel
      ansible.builtin.copy:
        dest: "./build_xlsx.py"
        mode: '0755'
        content: |
          import json, sys
          from openpyxl import Workbook
          from openpyxl.styles import Alignment, Font
          from openpyxl.utils import get_column_letter

          jpath, xpath = sys.argv[1], sys.argv[2]
          data = json.load(open(jpath, encoding='utf-8'))

          wb = Workbook()
          ws = wb.active
          ws.title = "external_links"

          headers = ["受控主机IP", "外部链接唯一IP个数", "外部链接IP(逐行)"]
          ws.append(headers)
          for c in range(1, len(headers)+1):
              cell = ws.cell(row=1, column=c)
              cell.font = Font(bold=True)
              cell.alignment = Alignment(horizontal="center", vertical="center")

          row = 2
          for item in data:
              host = item.get("host", "")
              ips = item.get("ips", []) or []
              cnt = int(item.get("count", 0))

              if not ips:
                  ws.append([host, cnt, ""])
                  row += 1
                  continue

              start = row
              for ip in ips:
                  ws.cell(row=row, column=3, value=ip)
                  ws.cell(row=row, column=3).alignment = Alignment(vertical="center")
                  row += 1
              end = row - 1

              ws.merge_cells(start_row=start, start_column=1, end_row=end, end_column=1)
              ws.merge_cells(start_row=start, start_column=2, end_row=end, end_column=2)
              ws.cell(row=start, column=1, value=host)
              ws.cell(row=start, column=2, value=cnt)

          widths = {1: 18, 2: 18, 3: 24}
          for c, w in widths.items():
              ws.column_dimensions[get_column_letter(c)].width = w
          wb.save(xpath)

    - name: Build XLSX
      ansible.builtin.command:
        cmd: python3 ./build_xlsx.py {{ json_path }} {{ xlsx_path }}

五、执行结果

执行:

ansible-playbook -i inventory.ini collect_external_conns.yml

输出:

  • external_links.json:原始数据;

  • external_links.xlsx:可直接打开查看的 Excel 报表。

即使部分主机没有外部连接,也会在报表中显示,如:

受控主机IP

外部链接唯一IP个数

外部链接IP(逐行)

10.0.1.10

4

100.58.230.144
100.58.70.170
...

10.0.1.20

0

(空)


六、总结

  • 使用 netstat 兼容性好,适用于老旧系统;

  • ::ffff: IPv6 映射需特别处理,否则会丢数据;

  • Excel 通过 openpyxl 自动合并单元格,阅读更直观;

  • 结果即使为空,也能完整展示所有主机,适合定期巡检或审计输出。


✅ 本方案结构清晰、兼容性强,可直接复用于运维巡检或安全合规报告中。
复制整段 YAML 后执行,即可快速生成企业级外部连接报表。

© 本文著作权归作者所有,未经许可不得转载使用。