Prometheus监控体系:内网node_exporter自动发现与配置更新脚本使用指南

一、文档概述

本文档介绍一款用于内网 node_exporter(9100 端口)自动化管理的 Python 脚本,核心功能包括:扫描指定内网网段的 9100 端口(node_exporter 默认端口)、检查 Prometheus 配置中是否已添加目标实例、自动补充未配置的实例、触发 Prometheus 配置热重载。脚本适配 Python 3.6 及以上版本,支持多线程扫描以提升效率,适用于内网监控场景下 node_exporter 实例的批量发现与配置。

二、核心功能清单

功能模块具体说明
多线程端口扫描基于线程池扫描指定内网网段,快速检测 9100 端口开放状态,支持多子网范围配置
Prometheus 配置读取解析 Prometheus.yml,提取目标 Job 下已配置的 node_exporter 实例
配置差异对比对比扫描结果与现有配置,筛选出未添加的新实例
配置自动更新将新实例以标准格式写入 Prometheus.yml 的目标 Job 中
配置热重载通过 curl 命令触发 Prometheus 配置重载(/-/reload接口),无需重启服务

三、环境依赖

1. 运行环境

  • Python 版本:3.6 及以上(兼容低版本 Python 的subprocess调用)
  • 系统依赖:需安装curl工具(用于触发 Prometheus 重载)
  • 第三方库:需安装pyyaml(用于解析 / 写入 YAML 格式的 Prometheus 配置)
1
pip install pyyaml

2. 目标环境

  • 内网环境:需与扫描的网段(如172.17.x.x)互通
  • Prometheus:需启用--web.enable-lifecycle参数(允许通过 API 重载配置)
    • 示例启动命令:prometheus --config.file=prometheus.yml --web.enable-lifecycle
    • 写在启动文件里:/etc/systemd/system/prometheus.service

四、脚本配置说明

脚本开头的配置参数区支持自定义调整,所有核心配置均集中在此处,无需修改代码逻辑,配置项说明如下:

配置项名称类型默认值 / 示例说明
NETWORK_PREFIX字符串"172.17."内网网段前缀,如"192.168.1."表示扫描192.168.1.x网段
SCAN_SUBNET_RANGES列表(元组)[(0, 0)]扫描的子网范围,格式为[(起始子网, 结束子网)],如[(0,2), (5,7)]表示扫描172.17.0.x~172.17.2.x172.17.5.x~172.17.7.x
SCAN_PORT整数9100扫描的端口号,固定为 node_exporter 默认端口9100
PROMETHEUS_CONFIG_PATH字符串"prometheus.yml"Prometheus 配置文件的路径(绝对路径或相对路径,需确保脚本有读写权限)
RELOAD_URL字符串"http://localhost:9090/-/reload"Prometheus 重载接口地址,需与 Prometheus 实际地址 / 端口匹配
TARGET_JOB_NAME字符串"xnyh_mini"Prometheus 中目标 Job 的名称(需与配置文件中job_name一致)
DEFAULT_TIMEOUT整数1端口连接超时时间(秒),建议保持 1~3 秒以平衡速度与准确性
DEFAULT_MAX_WORKERS整数64线程池最大线程数,控制扫描并发度(根据服务器性能调整,建议≤128)

定时执行:

1
30 03 * * * /usr/bin/python3 /data/scripts/Autodiscover_prot_prometheus.py >> /data/add_promttheus.log 2>&1

脚本内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import socket
import sys
import yaml
import subprocess
from concurrent.futures import ThreadPoolExecutor, as_completed

# 配置参数 - 在此处定义相关常量
NETWORK_PREFIX = "172.17." # 网段前缀
SCAN_SUBNET_RANGES = [(0, 2)] # 要扫描的子网范围,例如[(0, 2), (5, 7)]
SCAN_PORT = 9100 # node_exporter默认端口
PROMETHEUS_CONFIG_PATH = "/opt/prometheus/prometheus/prometheus.yml" # prometheus配置文件路径
RELOAD_URL = "http://localhost:9090/-/reload" # 重新加载地址
TARGET_JOB_NAME = "xnyh_mini" # 目标Job名称(在此处统一定义)
DEFAULT_TIMEOUT = 1 # 超时时间(秒)
DEFAULT_MAX_WORKERS = 64 # 最大线程数


def check_port(ip, port, timeout):
"""检查指定IP的指定端口是否开放"""
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(timeout)
result = sock.connect_ex((ip, port))
return result == 0
except socket.error:
return False


def scan_subnets():
"""扫描配置中定义的所有子网段落的指定端口"""
open_targets = []

with ThreadPoolExecutor(max_workers=DEFAULT_MAX_WORKERS) as executor:
for subnet in SCAN_SUBNET_RANGES:
start_subnet, end_subnet = subnet
for current_subnet in range(start_subnet, end_subnet + 1):
current_prefix = f"{NETWORK_PREFIX}{current_subnet}."
future_to_ip = {
executor.submit(check_port, f"{current_prefix}{i}", SCAN_PORT, DEFAULT_TIMEOUT):
f"{current_prefix}{i}:{SCAN_PORT}"
for i in range(1, 255)
}

for future in as_completed(future_to_ip):
target = future_to_ip[future]
try:
if future.result():
print(f"发现开放端口: {target}")
open_targets.append(target)
except Exception as exc:
print(f"扫描{target}时出错: {exc}")

return open_targets


def get_existing_targets():
"""从prometheus配置文件中获取已存在的targets"""
try:
with open(PROMETHEUS_CONFIG_PATH, 'r') as f:
config = yaml.safe_load(f)

# 使用开头定义的TARGET_JOB_NAME常量
for job in config.get('scrape_configs', []):
if job.get('job_name') == TARGET_JOB_NAME:
existing = set()
for static_config in job.get('static_configs', []):
existing.update(static_config.get('targets', []))
return existing
return set()
except Exception as e:
print(f"读取prometheus配置失败: {e}")
sys.exit(1)


def add_new_targets(new_targets):
"""向prometheus配置添加新的targets"""
if not new_targets:
return False

try:
with open(PROMETHEUS_CONFIG_PATH, 'r') as f:
config = yaml.safe_load(f)

# 使用开头定义的TARGET_JOB_NAME常量
for job in config.get('scrape_configs', []):
if job.get('job_name') == TARGET_JOB_NAME:
for target in new_targets:
# 保持与现有配置一致的格式(单个targets项)
job['static_configs'].append({'targets': [target]})
print(f"已添加新目标: {target}")

with open(PROMETHEUS_CONFIG_PATH, 'w') as f:
yaml.dump(config, f, sort_keys=False, default_flow_style=False, allow_unicode=True)
return True
return False
except Exception as e:
print(f"更新prometheus配置失败: {e}")
return False


def reload_prometheus():
"""发送重新加载请求到prometheus"""
try:
result = subprocess.run(
['curl', '-X', 'POST', RELOAD_URL],
check=True,
stdout=subprocess.PIPE, # 捕获标准输出
stderr=subprocess.PIPE, # 捕获标准错误
)
if result.returncode == 0:
print("Prometheus配置已成功重新加载")
else:
print("重新加载Prometheus失败:", result.stderr)
return False
return True
except subprocess.CalledProcessError as e:
print(f"重新加载Prometheus失败: {e.stderr}")
return False


def main():
print(f"开始扫描网段 {NETWORK_PREFIX} 下的子网 {SCAN_SUBNET_RANGES}{SCAN_PORT} 端口...\n")

open_targets = scan_subnets()
if not open_targets:
print("未发现开放的9100端口")
return

existing_targets = get_existing_targets()
print(f"\n已存在的目标数量: {len(existing_targets)}")

new_targets = [t for t in open_targets if t not in existing_targets]
if not new_targets:
print("没有需要添加的新目标")
return

print(f"\n需要添加的新目标数量: {len(new_targets)}")

if add_new_targets(new_targets):
reload_prometheus()


if __name__ == "__main__":
main()

五、脚本执行流程

1. 执行命令

脚本无需命令行参数(扫描范围已在配置中定义),直接运行即可:

1
python3 脚本文件名.py

2. 详细流程

  1. 初始化配置:加载开头定义的网段、端口、Job 名称等配置参数。
  2. 多线程端口扫描:
    • SCAN_SUBNET_RANGES遍历子网,生成172.17.x.1~172.17.x.254的 IP 列表。
    • 线程池提交check_port任务,检测每个 IP 的 9100 端口是否开放,收集开放的IP:端口列表。
  3. 读取现有配置:
    • 解析prometheus.yml,找到job_name=TARGET_JOB_NAME的 Job。
    • 提取该 Job 下static_configs中的所有targets,去重后生成现有实例集合。
  4. 筛选新实例:对比扫描结果与现有实例,得到需新增的目标列表。
  5. 更新配置文件:
    • 若存在新实例,以- targets: ['IP:端口']格式写入 Prometheus.yml 的目标 Job 中。
    • 保持 YAML 格式规范(禁用流式风格、保留原有键顺序)。
  6. 触发配置重载:通过curl -X POST RELOAD_URL调用 Prometheus 接口,热重载配置。

六、常见问题与解决方案

1. 报错:TypeError: __init__() got an unexpected keyword argument 'capture_output'

  • 原因:Python 3.6 及以下版本不支持subprocess.run()capture_output参数。
  • 解决方案:使用stdout=subprocess.PIPEstderr=subprocess.PIPE替代,脚本已默认适配(见reload_prometheus函数)。

2. 报错:读取prometheus配置失败: [Errno 13] Permission denied

  • 原因:脚本对PROMETHEUS_CONFIG_PATH指向的文件无读写权限。
  • 解决方案:执行chmod +rwx prometheus.yml(或对应路径)赋予权限,或使用 sudo 运行脚本。

3. 扫描到实例但未添加到配置

  • 原因 1:Prometheus.yml 中未找到job_name=TARGET_JOB_NAME的 Job。
    • 解决方案:检查TARGET_JOB_NAME是否与 Prometheus 配置中的 Job 名称完全一致(区分大小写)。
  • 原因 2:实例已存在于配置中(可能格式不同,如IP:9100IP: 9100)。
    • 解决方案:脚本会自动去重,无需手动处理,若仍有问题可检查配置文件中的目标格式。

4. 重载 Prometheus 失败:curl: (7) Failed to connect to localhost port 9090: Connection refused

  • 原因 1:Prometheus 未启动或端口错误。
    • 解决方案:检查 Prometheus 进程状态(ps -ef | grep prometheus),确认端口与RELOAD_URL一致。
  • 原因 2:Prometheus 未启用--web.enable-lifecycle参数。
    • 解决方案:重启 Prometheus 并添加该参数,确保重载接口可用。

七、注意事项

  1. 配置备份:脚本更新 Prometheus.yml 时会移除原有注释(YAML 解析器限制),建议首次运行前备份配置文件(如cp prometheus.yml prometheus.yml.bak)。
  2. 网段规划:避免扫描过大的网段(如[(0,255)]),建议按业务分区扫描(如[(0,10), (20,30)]),防止并发过高导致网络拥堵。
  3. 权限控制:脚本需读写 Prometheus 配置文件、执行 curl 命令,建议使用非 root 用户运行(需确保对应权限),避免权限过高带来安全风险。
  4. 日志记录:脚本仅在控制台输出信息,若需持久化日志,可将执行结果重定向到文件(如python 脚本名.py > scan_log.txt 2>&1)。

八、触类旁通-端口扫描

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import socket
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed

# 定义默认的端口和超时时间
NETWORK_PREFIX = "172.17." # 网段前缀
DEFAULT_PORT = 22
DEFAULT_TIMEOUT = 1 # 超时时间(秒)
DEFAULT_MAX_WORKERS = 64 # 最大线程数


def check_port(ip, port, timeout):
"""
检查指定IP的指定端口是否开放。

:param ip: 要检查的IP地址
:param port: 要检查的端口号
:param timeout: 连接超时时间
:return: 如果端口开放,返回True,否则返回False
"""
try:
# 创建一个TCP套接字
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
# 设置超时时间
sock.settimeout(timeout)
# 尝试连接
result = sock.connect_ex((ip, port))
if result == 0:
return True
except socket.error:
pass
return False


def scan_subnet(network_prefix, subnet_range, port, timeout, max_workers):
"""
扫描指定范围内的多个子网中指定端口的开放情况。

:param network_prefix: 网段前缀,例如 "10.4."
:param subnet_range: 子网范围,例如 (0, 99)
:param port: 要扫描的端口号
:param timeout: 连接超时时间
:param max_workers: 最大线程数
"""
open_ips = [] # 存储开放端口的IP列表

# 使用线程池加快扫描速度
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# 遍历子网范围
for subnet in range(subnet_range[0], subnet_range[1] + 1):
current_prefix = f"{network_prefix}{subnet}."
# 为当前子网的每个IP地址提交一个检查任务
future_to_ip = {executor.submit(check_port, f"{current_prefix}{i}", port, timeout): f"{current_prefix}{i}"
for i in range(1, 255)}

# 逐个获取完成的任务结果
for future in as_completed(future_to_ip):
ip = future_to_ip[future]
try:
if future.result():
print(f"端口 {port}{ip} 上是开放的。")
open_ips.append(ip)
except Exception as exc:
print(f"{ip} 生成异常: {exc}")

# 输出所有开放端口的IP
print("\n扫描完成。以下IP的端口开放:")
for ip in open_ips:
print(ip)


def parse_arguments():
"""
解析命令行参数,获取子网范围。

:return: 起始子网和结束子网
"""
if len(sys.argv) != 3:
print("使用方法: python port_scan.py 起始子网 结束子网")
print("例如: python port_scan.py 0 99")
sys.exit(1)
try:
start = int(sys.argv[1])
end = int(sys.argv[2])
if start < 0 or end > 255 or start > end:
raise ValueError
return (start, end)
except ValueError:
print("子网范围必须是0到255之间的整数,且起始子网不大于结束子网。")
sys.exit(1)


if __name__ == "__main__":
# 解析命令行参数获取子网范围
subnet_range = parse_arguments()

NETWORK_PREFIX = NETWORK_PREFIX # 网段前缀
PORT = DEFAULT_PORT # 要扫描的端口号
TIMEOUT = DEFAULT_TIMEOUT # 连接超时时间
MAX_WORKERS = DEFAULT_MAX_WORKERS # 最大线程数

print(f"开始扫描网段 {NETWORK_PREFIX}{subnet_range[0]}.0/24 到 {NETWORK_PREFIX}{subnet_range[1]}.0/24 的端口 {PORT}。\n")

scan_subnet(NETWORK_PREFIX, subnet_range, PORT, TIMEOUT, MAX_WORKERS)

执行命令:

1
python3 xxx.py 0 2